diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
commit | 51de1d8436100f725f3576aefa24a2bd2057bc28 (patch) | |
tree | c6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /misc | |
parent | Initial commit. (diff) | |
download | mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.tar.xz mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.zip |
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | misc/bstr.c | 469 | ||||
-rw-r--r-- | misc/bstr.h | 231 | ||||
-rw-r--r-- | misc/charset_conv.c | 235 | ||||
-rw-r--r-- | misc/charset_conv.h | 22 | ||||
-rw-r--r-- | misc/ctype.h | 19 | ||||
-rw-r--r-- | misc/dispatch.c | 417 | ||||
-rw-r--r-- | misc/dispatch.h | 32 | ||||
-rw-r--r-- | misc/jni.c | 429 | ||||
-rw-r--r-- | misc/jni.h | 161 | ||||
-rw-r--r-- | misc/json.c | 359 | ||||
-rw-r--r-- | misc/json.h | 31 | ||||
-rw-r--r-- | misc/language.c | 362 | ||||
-rw-r--r-- | misc/language.h | 31 | ||||
-rw-r--r-- | misc/linked_list.h | 107 | ||||
-rw-r--r-- | misc/natural_sort.c | 67 | ||||
-rw-r--r-- | misc/natural_sort.h | 23 | ||||
-rw-r--r-- | misc/node.c | 159 | ||||
-rw-r--r-- | misc/node.h | 20 | ||||
-rw-r--r-- | misc/random.c | 75 | ||||
-rw-r--r-- | misc/random.h | 41 | ||||
-rw-r--r-- | misc/rendezvous.c | 55 | ||||
-rw-r--r-- | misc/rendezvous.h | 8 | ||||
-rw-r--r-- | misc/thread_pool.c | 223 | ||||
-rw-r--r-- | misc/thread_pool.h | 35 | ||||
-rw-r--r-- | misc/thread_tools.c | 276 | ||||
-rw-r--r-- | misc/thread_tools.h | 83 | ||||
-rw-r--r-- | misc/uuid.c | 141 | ||||
-rw-r--r-- | misc/uuid.h | 146 |
28 files changed, 4257 insertions, 0 deletions
diff --git a/misc/bstr.c b/misc/bstr.c new file mode 100644 index 0000000..4f1e862 --- /dev/null +++ b/misc/bstr.c @@ -0,0 +1,469 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <string.h> +#include <strings.h> +#include <assert.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdlib.h> + +#include "mpv_talloc.h" + +#include "common/common.h" +#include "misc/ctype.h" +#include "bstr.h" + +int bstrcmp(struct bstr str1, struct bstr str2) +{ + int ret = 0; + if (str1.len && str2.len) + ret = memcmp(str1.start, str2.start, MPMIN(str1.len, str2.len)); + + if (!ret) { + if (str1.len == str2.len) + return 0; + else if (str1.len > str2.len) + return 1; + else + return -1; + } + return ret; +} + +int bstrcasecmp(struct bstr str1, struct bstr str2) +{ + int ret = 0; + if (str1.len && str2.len) + ret = strncasecmp(str1.start, str2.start, MPMIN(str1.len, str2.len)); + + if (!ret) { + if (str1.len == str2.len) + return 0; + else if (str1.len > str2.len) + return 1; + else + return -1; + } + return ret; +} + +int bstrchr(struct bstr str, int c) +{ + for (int i = 0; i < str.len; i++) + if (str.start[i] == c) + return i; + return -1; +} + +int bstrrchr(struct bstr str, int c) +{ + for (int i = str.len - 1; i >= 0; i--) + if (str.start[i] == c) + return i; + return -1; +} + +int bstrcspn(struct bstr str, const char *reject) +{ + int i; + for (i = 0; i < str.len; i++) + if (strchr(reject, str.start[i])) + break; + return i; +} + +int bstrspn(struct bstr str, const char *accept) +{ + int i; + for (i = 0; i < str.len; i++) + if (!strchr(accept, str.start[i])) + break; + return i; +} + +int bstr_find(struct bstr haystack, struct bstr needle) +{ + for (int i = 0; i < haystack.len; i++) + if (bstr_startswith(bstr_splice(haystack, i, haystack.len), needle)) + return i; + return -1; +} + +struct bstr bstr_lstrip(struct bstr str) +{ + while (str.len && mp_isspace(*str.start)) { + str.start++; + str.len--; + } + return str; +} + +struct bstr bstr_strip(struct bstr str) +{ + str = bstr_lstrip(str); + while (str.len && mp_isspace(str.start[str.len - 1])) + str.len--; + return str; +} + +struct bstr bstr_split(struct bstr str, const char *sep, struct bstr *rest) +{ + int start; + for (start = 0; start < str.len; start++) + if (!strchr(sep, str.start[start])) + break; + str = bstr_cut(str, start); + int end = bstrcspn(str, sep); + if (rest) { + *rest = bstr_cut(str, end); + } + return bstr_splice(str, 0, end); +} + +// Unlike with bstr_split(), tok is a string, and not a set of char. +// If tok is in str, return true, and: concat(out_left, tok, out_right) == str +// Otherwise, return false, and set out_left==str, out_right=="" +bool bstr_split_tok(bstr str, const char *tok, bstr *out_left, bstr *out_right) +{ + bstr bsep = bstr0(tok); + int pos = bstr_find(str, bsep); + if (pos < 0) + pos = str.len; + *out_left = bstr_splice(str, 0, pos); + *out_right = bstr_cut(str, pos + bsep.len); + return pos != str.len; +} + +struct bstr bstr_splice(struct bstr str, int start, int end) +{ + if (start < 0) + start += str.len; + if (end < 0) + end += str.len; + end = MPMIN(end, str.len); + start = MPMAX(start, 0); + end = MPMAX(end, start); + str.start += start; + str.len = end - start; + return str; +} + +long long bstrtoll(struct bstr str, struct bstr *rest, int base) +{ + str = bstr_lstrip(str); + char buf[51]; + int len = MPMIN(str.len, 50); + memcpy(buf, str.start, len); + buf[len] = 0; + char *endptr; + long long r = strtoll(buf, &endptr, base); + if (rest) + *rest = bstr_cut(str, endptr - buf); + return r; +} + +double bstrtod(struct bstr str, struct bstr *rest) +{ + str = bstr_lstrip(str); + char buf[101]; + int len = MPMIN(str.len, 100); + memcpy(buf, str.start, len); + buf[len] = 0; + char *endptr; + double r = strtod(buf, &endptr); + if (rest) + *rest = bstr_cut(str, endptr - buf); + return r; +} + +struct bstr bstr_splitchar(struct bstr str, struct bstr *rest, const char c) +{ + int pos = bstrchr(str, c); + if (pos < 0) + pos = str.len; + if (rest) + *rest = bstr_cut(str, pos + 1); + return bstr_splice(str, 0, pos + 1); +} + +struct bstr bstr_strip_linebreaks(struct bstr str) +{ + if (bstr_endswith0(str, "\r\n")) { + str = bstr_splice(str, 0, str.len - 2); + } else if (bstr_endswith0(str, "\n")) { + str = bstr_splice(str, 0, str.len - 1); + } + return str; +} + +bool bstr_eatstart(struct bstr *s, struct bstr prefix) +{ + if (!bstr_startswith(*s, prefix)) + return false; + *s = bstr_cut(*s, prefix.len); + return true; +} + +bool bstr_eatend(struct bstr *s, struct bstr prefix) +{ + if (!bstr_endswith(*s, prefix)) + return false; + s->len -= prefix.len; + return true; +} + +void bstr_lower(struct bstr str) +{ + for (int i = 0; i < str.len; i++) + str.start[i] = mp_tolower(str.start[i]); +} + +int bstr_sscanf(struct bstr str, const char *format, ...) +{ + char *ptr = bstrdup0(NULL, str); + va_list va; + va_start(va, format); + int ret = vsscanf(ptr, format, va); + va_end(va); + talloc_free(ptr); + return ret; +} + +int bstr_parse_utf8_code_length(unsigned char b) +{ + if (b < 128) + return 1; + int bytes = 7 - mp_log2(b ^ 255); + return (bytes >= 2 && bytes <= 4) ? bytes : -1; +} + +int bstr_decode_utf8(struct bstr s, struct bstr *out_next) +{ + if (s.len == 0) + return -1; + unsigned int codepoint = s.start[0]; + s.start++; s.len--; + if (codepoint >= 128) { + int bytes = bstr_parse_utf8_code_length(codepoint); + if (bytes < 1 || s.len < bytes - 1) + return -1; + codepoint &= 127 >> bytes; + for (int n = 1; n < bytes; n++) { + int tmp = (unsigned char)s.start[0]; + if ((tmp & 0xC0) != 0x80) + return -1; + codepoint = (codepoint << 6) | (tmp & ~0xC0); + s.start++; s.len--; + } + if (codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) + return -1; + // Overlong sequences - check taken from libavcodec. + // (The only reason we even bother with this is to make libavcodec's + // retarded subtitle utf-8 check happy.) + unsigned int min = bytes == 2 ? 0x80 : 1 << (5 * bytes - 4); + if (codepoint < min) + return -1; + } + if (out_next) + *out_next = s; + return codepoint; +} + +struct bstr bstr_split_utf8(struct bstr str, struct bstr *out_next) +{ + bstr rest; + int code = bstr_decode_utf8(str, &rest); + if (code < 0) + return (bstr){0}; + if (out_next) + *out_next = rest; + return bstr_splice(str, 0, str.len - rest.len); +} + +int bstr_validate_utf8(struct bstr s) +{ + while (s.len) { + if (bstr_decode_utf8(s, &s) < 0) { + // Try to guess whether the sequence was just cut-off. + unsigned int codepoint = (unsigned char)s.start[0]; + int bytes = bstr_parse_utf8_code_length(codepoint); + if (bytes > 1 && s.len < 6) { + // Manually check validity of left bytes + for (int n = 1; n < bytes; n++) { + if (n >= s.len) { + // Everything valid until now - just cut off. + return -(bytes - s.len); + } + int tmp = (unsigned char)s.start[n]; + if ((tmp & 0xC0) != 0x80) + break; + } + } + return -8; + } + } + return 0; +} + +struct bstr bstr_sanitize_utf8_latin1(void *talloc_ctx, struct bstr s) +{ + bstr new = {0}; + bstr left = s; + unsigned char *first_ok = s.start; + while (left.len) { + int r = bstr_decode_utf8(left, &left); + if (r < 0) { + bstr_xappend(talloc_ctx, &new, (bstr){first_ok, left.start - first_ok}); + mp_append_utf8_bstr(talloc_ctx, &new, (unsigned char)left.start[0]); + left.start += 1; + left.len -= 1; + first_ok = left.start; + } + } + if (!new.start) + return s; + if (first_ok != left.start) + bstr_xappend(talloc_ctx, &new, (bstr){first_ok, left.start - first_ok}); + return new; +} + +static void resize_append(void *talloc_ctx, bstr *s, size_t append_min) +{ + size_t size = talloc_get_size(s->start); + assert(s->len <= size); + if (append_min > size - s->len) { + if (append_min < size) + append_min = size; // preallocate in power of 2s + if (size >= SIZE_MAX / 2 || append_min >= SIZE_MAX / 2) + abort(); // oom + s->start = talloc_realloc_size(talloc_ctx, s->start, size + append_min); + } +} + +// Append the string, so that *s = *s + append. s->start is expected to be +// a talloc allocation (which can be realloced) or NULL. +// This function will always implicitly append a \0 after the new string for +// convenience. +// talloc_ctx will be used as parent context, if s->start is NULL. +void bstr_xappend(void *talloc_ctx, bstr *s, bstr append) +{ + if (!append.len) + return; + resize_append(talloc_ctx, s, append.len + 1); + memcpy(s->start + s->len, append.start, append.len); + s->len += append.len; + s->start[s->len] = '\0'; +} + +void bstr_xappend_asprintf(void *talloc_ctx, bstr *s, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + bstr_xappend_vasprintf(talloc_ctx, s, fmt, ap); + va_end(ap); +} + +// Exactly as bstr_xappend(), but with a formatted string. +void bstr_xappend_vasprintf(void *talloc_ctx, bstr *s, const char *fmt, + va_list ap) +{ + int size; + va_list copy; + va_copy(copy, ap); + size_t avail = talloc_get_size(s->start) - s->len; + char *dest = s->start ? s->start + s->len : NULL; + char c; + if (avail < 1) + dest = &c; + size = vsnprintf(dest, MPMAX(avail, 1), fmt, copy); + va_end(copy); + + if (size < 0) + abort(); + + if (avail < 1 || size + 1 > avail) { + resize_append(talloc_ctx, s, size + 1); + vsnprintf(s->start + s->len, size + 1, fmt, ap); + } + s->len += size; +} + +bool bstr_case_startswith(struct bstr s, struct bstr prefix) +{ + struct bstr start = bstr_splice(s, 0, prefix.len); + return start.len == prefix.len && bstrcasecmp(start, prefix) == 0; +} + +bool bstr_case_endswith(struct bstr s, struct bstr suffix) +{ + struct bstr end = bstr_cut(s, -suffix.len); + return end.len == suffix.len && bstrcasecmp(end, suffix) == 0; +} + +struct bstr bstr_strip_ext(struct bstr str) +{ + int dotpos = bstrrchr(str, '.'); + if (dotpos < 0) + return str; + return (struct bstr){str.start, dotpos}; +} + +struct bstr bstr_get_ext(struct bstr s) +{ + int dotpos = bstrrchr(s, '.'); + if (dotpos < 0) + return (struct bstr){NULL, 0}; + return bstr_splice(s, dotpos + 1, s.len); +} + +static int h_to_i(unsigned char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + + return -1; // invalid char +} + +bool bstr_decode_hex(void *talloc_ctx, struct bstr hex, struct bstr *out) +{ + if (!out) + return false; + + char *arr = talloc_array(talloc_ctx, char, hex.len / 2); + int len = 0; + + while (hex.len >= 2) { + int a = h_to_i(hex.start[0]); + int b = h_to_i(hex.start[1]); + hex = bstr_splice(hex, 2, hex.len); + + if (a < 0 || b < 0) { + talloc_free(arr); + return false; + } + + arr[len++] = (a << 4) | b; + } + + *out = (struct bstr){ .start = arr, .len = len }; + return true; +} diff --git a/misc/bstr.h b/misc/bstr.h new file mode 100644 index 0000000..dc8ad40 --- /dev/null +++ b/misc/bstr.h @@ -0,0 +1,231 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MPLAYER_BSTR_H +#define MPLAYER_BSTR_H + +#include <stdint.h> +#include <stddef.h> +#include <string.h> +#include <stdbool.h> +#include <stdarg.h> + +#include "mpv_talloc.h" +#include "osdep/compiler.h" + +/* NOTE: 'len' is size_t, but most string-handling functions below assume + * that input size has been sanity checked and len fits in an int. + */ +typedef struct bstr { + unsigned char *start; + size_t len; +} bstr; + +// If str.start is NULL, return NULL. +static inline char *bstrdup0(void *talloc_ctx, struct bstr str) +{ + return talloc_strndup(talloc_ctx, (char *)str.start, str.len); +} + +// Like bstrdup0(), but always return a valid C-string. +static inline char *bstrto0(void *talloc_ctx, struct bstr str) +{ + return str.start ? bstrdup0(talloc_ctx, str) : talloc_strdup(talloc_ctx, ""); +} + +// Return start = NULL iff that is true for the original. +static inline struct bstr bstrdup(void *talloc_ctx, struct bstr str) +{ + struct bstr r = { NULL, str.len }; + if (str.start) + r.start = (unsigned char *)talloc_memdup(talloc_ctx, str.start, str.len); + return r; +} + +static inline struct bstr bstr0(const char *s) +{ + return (struct bstr){(unsigned char *)s, s ? strlen(s) : 0}; +} + +int bstrcmp(struct bstr str1, struct bstr str2); +int bstrcasecmp(struct bstr str1, struct bstr str2); +int bstrchr(struct bstr str, int c); +int bstrrchr(struct bstr str, int c); +int bstrspn(struct bstr str, const char *accept); +int bstrcspn(struct bstr str, const char *reject); + +int bstr_find(struct bstr haystack, struct bstr needle); +struct bstr bstr_lstrip(struct bstr str); +struct bstr bstr_strip(struct bstr str); +struct bstr bstr_split(struct bstr str, const char *sep, struct bstr *rest); +bool bstr_split_tok(bstr str, const char *tok, bstr *out_left, bstr *out_right); +struct bstr bstr_splice(struct bstr str, int start, int end); +long long bstrtoll(struct bstr str, struct bstr *rest, int base); +double bstrtod(struct bstr str, struct bstr *rest); +void bstr_lower(struct bstr str); +int bstr_sscanf(struct bstr str, const char *format, ...); + +// Decode a string containing hexadecimal data. All whitespace will be silently +// ignored. When successful, this allocates a new array to store the output. +bool bstr_decode_hex(void *talloc_ctx, struct bstr hex, struct bstr *out); + +// Decode the UTF-8 code point at the start of the string, and return the +// character. +// After calling this function, *out_next will point to the next character. +// out_next can be NULL. +// On error, -1 is returned, and *out_next is not modified. +int bstr_decode_utf8(struct bstr str, struct bstr *out_next); + +// Return the UTF-8 code point at the start of the string. +// After calling this function, *out_next will point to the next character. +// out_next can be NULL. +// On error, an empty string is returned, and *out_next is not modified. +struct bstr bstr_split_utf8(struct bstr str, struct bstr *out_next); + +// Return the length of the UTF-8 sequence that starts with the given byte. +// Given a string char *s, the next UTF-8 code point is to be expected at +// s + bstr_parse_utf8_code_length(s[0]) +// On error, -1 is returned. On success, it returns a value in the range [1, 4]. +int bstr_parse_utf8_code_length(unsigned char b); + +// Return >= 0 if the string is valid UTF-8, otherwise negative error code. +// Embedded \0 bytes are considered valid. +// This returns -N if the UTF-8 string was likely just cut-off in the middle of +// an UTF-8 sequence: -1 means 1 byte was missing, -5 5 bytes missing. +// If the string was likely not cut off, -8 is returned. +// Use (return_value > -8) to check whether the string is valid UTF-8 or valid +// but cut-off UTF-8. +int bstr_validate_utf8(struct bstr s); + +// Force the input string to valid UTF-8. If invalid UTF-8 encoding is +// encountered, the invalid bytes are interpreted as Latin-1. +// Embedded \0 bytes are considered valid. +// If replacement happens, a newly allocated string is returned (with a \0 +// byte added past its end for convenience). The string is allocated via +// talloc, with talloc_ctx as parent. +struct bstr bstr_sanitize_utf8_latin1(void *talloc_ctx, struct bstr s); + +// Return the text before the occurrence of a character, and return it. Change +// *rest to point to the text following this character. (rest can be NULL.) +struct bstr bstr_splitchar(struct bstr str, struct bstr *rest, const char c); + +// Like bstr_splitchar. Trailing newlines are not stripped. +static inline struct bstr bstr_getline(struct bstr str, struct bstr *rest) +{ + return bstr_splitchar(str, rest, '\n'); +} + +// Strip one trailing line break. This is intended for use with bstr_getline, +// and will remove the trailing \n or \r\n sequence. +struct bstr bstr_strip_linebreaks(struct bstr str); + +void bstr_xappend(void *talloc_ctx, bstr *s, bstr append); +void bstr_xappend_asprintf(void *talloc_ctx, bstr *s, const char *fmt, ...) + PRINTF_ATTRIBUTE(3, 4); +void bstr_xappend_vasprintf(void *talloc_ctx, bstr *s, const char *fmt, va_list va) + PRINTF_ATTRIBUTE(3, 0); + +// If s starts/ends with prefix, return true and return the rest of the string +// in s. +bool bstr_eatstart(struct bstr *s, struct bstr prefix); +bool bstr_eatend(struct bstr *s, struct bstr prefix); + +bool bstr_case_startswith(struct bstr s, struct bstr prefix); +bool bstr_case_endswith(struct bstr s, struct bstr suffix); +struct bstr bstr_strip_ext(struct bstr str); +struct bstr bstr_get_ext(struct bstr s); + +static inline struct bstr bstr_cut(struct bstr str, int n) +{ + if (n < 0) { + n += str.len; + if (n < 0) + n = 0; + } + if (((size_t)n) > str.len) + n = str.len; + return (struct bstr){str.start + n, str.len - n}; +} + +static inline bool bstr_startswith(struct bstr str, struct bstr prefix) +{ + if (str.len < prefix.len) + return false; + return !memcmp(str.start, prefix.start, prefix.len); +} + +static inline bool bstr_startswith0(struct bstr str, const char *prefix) +{ + return bstr_startswith(str, bstr0(prefix)); +} + +static inline bool bstr_endswith(struct bstr str, struct bstr suffix) +{ + if (str.len < suffix.len) + return false; + return !memcmp(str.start + str.len - suffix.len, suffix.start, suffix.len); +} + +static inline bool bstr_endswith0(struct bstr str, const char *suffix) +{ + return bstr_endswith(str, bstr0(suffix)); +} + +static inline int bstrcmp0(struct bstr str1, const char *str2) +{ + return bstrcmp(str1, bstr0(str2)); +} + +static inline bool bstr_equals(struct bstr str1, struct bstr str2) +{ + if (str1.len != str2.len) + return false; + + return str1.start == str2.start || bstrcmp(str1, str2) == 0; +} + +static inline bool bstr_equals0(struct bstr str1, const char *str2) +{ + return bstr_equals(str1, bstr0(str2)); +} + +static inline int bstrcasecmp0(struct bstr str1, const char *str2) +{ + return bstrcasecmp(str1, bstr0(str2)); +} + +static inline int bstr_find0(struct bstr haystack, const char *needle) +{ + return bstr_find(haystack, bstr0(needle)); +} + +static inline bool bstr_eatstart0(struct bstr *s, const char *prefix) +{ + return bstr_eatstart(s, bstr0(prefix)); +} + +static inline bool bstr_eatend0(struct bstr *s, const char *prefix) +{ + return bstr_eatend(s, bstr0(prefix)); +} + +// create a pair (not single value!) for "%.*s" printf syntax +#define BSTR_P(bstr) (int)((bstr).len), ((bstr).start ? (char*)(bstr).start : "") + +#define WHITESPACE " \f\n\r\t\v" + +#endif /* MPLAYER_BSTR_H */ diff --git a/misc/charset_conv.c b/misc/charset_conv.c new file mode 100644 index 0000000..b54f636 --- /dev/null +++ b/misc/charset_conv.c @@ -0,0 +1,235 @@ +/* + * This file is part of mpv. + * + * Based on code taken from libass (ISC license), which was originally part + * of MPlayer (GPL). + * Copyright (C) 2006 Evgeniy Stepanov <eugeni.stepanov@gmail.com> + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> +#include <errno.h> +#include <strings.h> +#include <assert.h> + +#include "config.h" + +#include "common/msg.h" + +#if HAVE_UCHARDET +#include <uchardet.h> +#endif + +#if HAVE_ICONV +#include <iconv.h> +#endif + +#include "charset_conv.h" + +bool mp_charset_is_utf8(const char *user_cp) +{ + return user_cp && (strcasecmp(user_cp, "utf8") == 0 || + strcasecmp(user_cp, "utf-8") == 0); +} + +bool mp_charset_is_utf16(const char *user_cp) +{ + bstr s = bstr0(user_cp); + return bstr_case_startswith(s, bstr0("utf16")) || + bstr_case_startswith(s, bstr0("utf-16")); +} + +static const char *const utf_bom[3] = {"\xEF\xBB\xBF", "\xFF\xFE", "\xFE\xFF"}; +static const char *const utf_enc[3] = {"utf-8", "utf-16le", "utf-16be"}; + +static const char *ms_bom_guess(bstr buf) +{ + for (int n = 0; n < 3; n++) { + if (bstr_startswith0(buf, utf_bom[n])) + return utf_enc[n]; + } + return NULL; +} + +#if HAVE_UCHARDET +static const char *mp_uchardet(void *talloc_ctx, struct mp_log *log, bstr buf) +{ + uchardet_t det = uchardet_new(); + if (!det) + return NULL; + if (uchardet_handle_data(det, buf.start, buf.len) != 0) { + uchardet_delete(det); + return NULL; + } + uchardet_data_end(det); + char *res = talloc_strdup(talloc_ctx, uchardet_get_charset(det)); + if (res && !res[0]) + res = NULL; + if (res) { + mp_verbose(log, "libuchardet detected charset as %s\n", res); + iconv_t icdsc = iconv_open("UTF-8", res); + if (icdsc == (iconv_t)(-1)) { + mp_warn(log, "Charset '%s' not supported by iconv.\n", res); + res = NULL; + } else { + iconv_close(icdsc); + } + } + uchardet_delete(det); + return res; +} +#endif + +// Runs charset auto-detection on the input buffer, and returns the result. +// If auto-detection fails, NULL is returned. +// If user_cp doesn't refer to any known auto-detection (for example because +// it's a real iconv codepage), user_cp is returned without even looking at +// the buf data. +// The return value may (but doesn't have to) be allocated under talloc_ctx. +const char *mp_charset_guess(void *talloc_ctx, struct mp_log *log, bstr buf, + const char *user_cp, int flags) +{ + if (user_cp[0] == '+') { + mp_verbose(log, "Forcing charset '%s'.\n", user_cp + 1); + return user_cp + 1; + } + + const char *bom_cp = ms_bom_guess(buf); + if (bom_cp) { + mp_verbose(log, "Data has a BOM, assuming %s as charset.\n", bom_cp); + return bom_cp; + } + + int r = bstr_validate_utf8(buf); + if (r >= 0 || (r > -8 && (flags & MP_ICONV_ALLOW_CUTOFF))) { + if (strcmp(user_cp, "auto") != 0 && !mp_charset_is_utf8(user_cp)) + mp_verbose(log, "Data looks like UTF-8, ignoring user-provided charset.\n"); + return "utf-8"; + } + + const char *res = NULL; + if (strcasecmp(user_cp, "auto") == 0) { +#if HAVE_UCHARDET + res = mp_uchardet(talloc_ctx, log, buf); +#endif + if (!res) { + mp_verbose(log, "Charset auto-detection failed.\n"); + res = "UTF-8-BROKEN"; + } + } else { + res = user_cp; + } + + mp_verbose(log, "Using charset '%s'.\n", res); + return res; +} + +// Use iconv to convert buf to UTF-8. +// Returns buf.start==NULL on error. Returns buf if cp is NULL, or if there is +// obviously no conversion required (e.g. if cp is "UTF-8"). +// Returns a newly allocated buffer if conversion is done and succeeds. The +// buffer will be terminated with 0 for convenience (the terminating 0 is not +// included in the returned length). +// Free the returned buffer with talloc_free(). +// buf: input data +// cp: iconv codepage (or NULL) +// flags: combination of MP_ICONV_* flags +// returns: buf (no conversion), .start==NULL (error), or allocated buffer +bstr mp_iconv_to_utf8(struct mp_log *log, bstr buf, const char *cp, int flags) +{ +#if HAVE_ICONV + if (!buf.len) + return buf; + + if (!cp || !cp[0] || mp_charset_is_utf8(cp)) + return buf; + + if (strcasecmp(cp, "ASCII") == 0) + return buf; + + if (strcasecmp(cp, "UTF-8-BROKEN") == 0) + return bstr_sanitize_utf8_latin1(NULL, buf); + + // Force CP949 over EUC-KR since iconv distinguishes them and + // EUC-KR causes error on CP949 encoded data + if (strcasecmp(cp, "EUC-KR") == 0) + cp = "CP949"; + + iconv_t icdsc; + if ((icdsc = iconv_open("UTF-8", cp)) == (iconv_t) (-1)) { + if (flags & MP_ICONV_VERBOSE) + mp_err(log, "Error opening iconv with codepage '%s'\n", cp); + goto failure; + } + + size_t size = buf.len; + size_t osize = size; + size_t ileft = size; + size_t oleft = size - 1; + + char *outbuf = talloc_size(NULL, osize); + char *ip = buf.start; + char *op = outbuf; + + while (1) { + int clear = 0; + size_t rc; + if (ileft) + rc = iconv(icdsc, &ip, &ileft, &op, &oleft); + else { + clear = 1; // clear the conversion state and leave + rc = iconv(icdsc, NULL, NULL, &op, &oleft); + } + if (rc == (size_t) (-1)) { + if (errno == E2BIG) { + size_t offset = op - outbuf; + outbuf = talloc_realloc_size(NULL, outbuf, osize + size); + op = outbuf + offset; + osize += size; + oleft += size; + } else { + if (errno == EINVAL && (flags & MP_ICONV_ALLOW_CUTOFF)) { + // This is intended for cases where the input buffer is cut + // at a random byte position. If this happens in the middle + // of the buffer, it should still be an error. We say it's + // fine if the error is within 10 bytes of the end. + if (ileft <= 10) + break; + } + if (flags & MP_ICONV_VERBOSE) { + mp_err(log, "Error recoding text with codepage '%s'\n", cp); + } + talloc_free(outbuf); + iconv_close(icdsc); + goto failure; + } + } else if (clear) + break; + } + + iconv_close(icdsc); + + outbuf[osize - oleft - 1] = 0; + return (bstr){outbuf, osize - oleft - 1}; + +failure: +#endif + + if (flags & MP_NO_LATIN1_FALLBACK) { + return buf; + } else { + return bstr_sanitize_utf8_latin1(NULL, buf); + } +} diff --git a/misc/charset_conv.h b/misc/charset_conv.h new file mode 100644 index 0000000..ccaa17e --- /dev/null +++ b/misc/charset_conv.h @@ -0,0 +1,22 @@ +#ifndef MP_CHARSET_CONV_H +#define MP_CHARSET_CONV_H + +#include <stdbool.h> +#include "misc/bstr.h" + +struct mp_log; + +enum { + MP_ICONV_VERBOSE = 1, // print errors instead of failing silently + MP_ICONV_ALLOW_CUTOFF = 2, // allow partial input data + MP_STRICT_UTF8 = 4, // don't fall back to UTF-8-BROKEN when guessing + MP_NO_LATIN1_FALLBACK = 8, // fall back to input buffer instead of latin1 +}; + +bool mp_charset_is_utf8(const char *user_cp); +bool mp_charset_is_utf16(const char *user_cp); +const char *mp_charset_guess(void *talloc_ctx, struct mp_log *log, bstr buf, + const char *user_cp, int flags); +bstr mp_iconv_to_utf8(struct mp_log *log, bstr buf, const char *cp, int flags); + +#endif diff --git a/misc/ctype.h b/misc/ctype.h new file mode 100644 index 0000000..cbff799 --- /dev/null +++ b/misc/ctype.h @@ -0,0 +1,19 @@ +#ifndef MP_CTYPE_H_ +#define MP_CTYPE_H_ + +// Roughly follows C semantics, but doesn't account for EOF, allows char as +// parameter, and is locale independent (always uses "C" locale). + +static inline int mp_isprint(char c) { return (unsigned char)c >= 32; } +static inline int mp_isspace(char c) { return c == ' ' || c == '\f' || c == '\n' || + c == '\r' || c == '\t' || c =='\v'; } +static inline int mp_isupper(char c) { return c >= 'A' && c <= 'Z'; } +static inline int mp_islower(char c) { return c >= 'a' && c <= 'z'; } +static inline int mp_isdigit(char c) { return c >= '0' && c <= '9'; } +static inline int mp_isalpha(char c) { return mp_isupper(c) || mp_islower(c); } +static inline int mp_isalnum(char c) { return mp_isalpha(c) || mp_isdigit(c); } + +static inline char mp_tolower(char c) { return mp_isupper(c) ? c - 'A' + 'a' : c; } +static inline char mp_toupper(char c) { return mp_islower(c) ? c - 'a' + 'A' : c; } + +#endif diff --git a/misc/dispatch.c b/misc/dispatch.c new file mode 100644 index 0000000..6fd9fe1 --- /dev/null +++ b/misc/dispatch.c @@ -0,0 +1,417 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdbool.h> +#include <assert.h> + +#include "common/common.h" +#include "osdep/threads.h" +#include "osdep/timer.h" + +#include "dispatch.h" + +struct mp_dispatch_queue { + struct mp_dispatch_item *head, *tail; + mp_mutex lock; + mp_cond cond; + void (*wakeup_fn)(void *wakeup_ctx); + void *wakeup_ctx; + void (*onlock_fn)(void *onlock_ctx); + void *onlock_ctx; + // Time at which mp_dispatch_queue_process() should return. + int64_t wait; + // Make mp_dispatch_queue_process() exit if it's idle. + bool interrupted; + // The target thread is in mp_dispatch_queue_process() (and either idling, + // locked, or running a dispatch callback). + bool in_process; + mp_thread_id in_process_thread_id; + // The target thread is in mp_dispatch_queue_process(), and currently + // something has exclusive access to it (e.g. running a dispatch callback, + // or a different thread got it with mp_dispatch_lock()). + bool locked; + // A mp_dispatch_lock() call is requesting an exclusive lock. + size_t lock_requests; + // locked==true is due to a mp_dispatch_lock() call (for debugging). + bool locked_explicit; + mp_thread_id locked_explicit_thread_id; +}; + +struct mp_dispatch_item { + mp_dispatch_fn fn; + void *fn_data; + bool asynchronous; + bool mergeable; + bool completed; + struct mp_dispatch_item *next; +}; + +static void queue_dtor(void *p) +{ + struct mp_dispatch_queue *queue = p; + assert(!queue->head); + assert(!queue->in_process); + assert(!queue->lock_requests); + assert(!queue->locked); + mp_cond_destroy(&queue->cond); + mp_mutex_destroy(&queue->lock); +} + +// A dispatch queue lets other threads run callbacks in a target thread. +// The target thread is the thread which calls mp_dispatch_queue_process(). +// Free the dispatch queue with talloc_free(). At the time of destruction, +// the queue must be empty. The easiest way to guarantee this is to +// terminate all potential senders, then call mp_dispatch_run() with a +// function that e.g. makes the target thread exit, then mp_thread_join() the +// target thread, and finally destroy the queue. Another way is calling +// mp_dispatch_queue_process() after terminating all potential senders, and +// then destroying the queue. +struct mp_dispatch_queue *mp_dispatch_create(void *ta_parent) +{ + struct mp_dispatch_queue *queue = talloc_ptrtype(ta_parent, queue); + *queue = (struct mp_dispatch_queue){0}; + talloc_set_destructor(queue, queue_dtor); + mp_mutex_init(&queue->lock); + mp_cond_init(&queue->cond); + return queue; +} + +// Set a custom function that should be called to guarantee that the target +// thread wakes up. This is intended for use with code that needs to block +// on non-pthread primitives, such as e.g. select(). In the case of select(), +// the wakeup_fn could for example write a byte into a "wakeup" pipe in order +// to unblock the select(). The wakeup_fn is called from the dispatch queue +// when there are new dispatch items, and the target thread should then enter +// mp_dispatch_queue_process() as soon as possible. +// Note that this setter does not do internal synchronization, so you must set +// it before other threads see it. +void mp_dispatch_set_wakeup_fn(struct mp_dispatch_queue *queue, + void (*wakeup_fn)(void *wakeup_ctx), + void *wakeup_ctx) +{ + queue->wakeup_fn = wakeup_fn; + queue->wakeup_ctx = wakeup_ctx; +} + +// Set a function that will be called by mp_dispatch_lock() if the target thread +// is not calling mp_dispatch_queue_process() right now. This is an obscure, +// optional mechanism to make a worker thread react to external events more +// quickly. The idea is that the callback will make the worker thread to stop +// doing whatever (e.g. by setting a flag), and call mp_dispatch_queue_process() +// in order to let mp_dispatch_lock() calls continue sooner. +// Like wakeup_fn, this setter does no internal synchronization, and you must +// not access the dispatch queue itself from the callback. +void mp_dispatch_set_onlock_fn(struct mp_dispatch_queue *queue, + void (*onlock_fn)(void *onlock_ctx), + void *onlock_ctx) +{ + queue->onlock_fn = onlock_fn; + queue->onlock_ctx = onlock_ctx; +} + +static void mp_dispatch_append(struct mp_dispatch_queue *queue, + struct mp_dispatch_item *item) +{ + mp_mutex_lock(&queue->lock); + if (item->mergeable) { + for (struct mp_dispatch_item *cur = queue->head; cur; cur = cur->next) { + if (cur->mergeable && cur->fn == item->fn && + cur->fn_data == item->fn_data) + { + talloc_free(item); + mp_mutex_unlock(&queue->lock); + return; + } + } + } + + if (queue->tail) { + queue->tail->next = item; + } else { + queue->head = item; + } + queue->tail = item; + + // Wake up the main thread; note that other threads might wait on this + // condition for reasons, so broadcast the condition. + mp_cond_broadcast(&queue->cond); + // No wakeup callback -> assume mp_dispatch_queue_process() needs to be + // interrupted instead. + if (!queue->wakeup_fn) + queue->interrupted = true; + mp_mutex_unlock(&queue->lock); + + if (queue->wakeup_fn) + queue->wakeup_fn(queue->wakeup_ctx); +} + +// Enqueue a callback to run it on the target thread asynchronously. The target +// thread will run fn(fn_data) as soon as it enter mp_dispatch_queue_process. +// Note that mp_dispatch_enqueue() will usually return long before that happens. +// It's up to the user to signal completion of the callback. It's also up to +// the user to guarantee that the context fn_data has correct lifetime, i.e. +// lives until the callback is run, and is freed after that. +void mp_dispatch_enqueue(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data) +{ + struct mp_dispatch_item *item = talloc_ptrtype(NULL, item); + *item = (struct mp_dispatch_item){ + .fn = fn, + .fn_data = fn_data, + .asynchronous = true, + }; + mp_dispatch_append(queue, item); +} + +// Like mp_dispatch_enqueue(), but the queue code will call talloc_free(fn_data) +// after the fn callback has been run. (The callback could trivially do that +// itself, but it makes it easier to implement synchronous and asynchronous +// requests with the same callback implementation.) +void mp_dispatch_enqueue_autofree(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data) +{ + struct mp_dispatch_item *item = talloc_ptrtype(NULL, item); + *item = (struct mp_dispatch_item){ + .fn = fn, + .fn_data = talloc_steal(item, fn_data), + .asynchronous = true, + }; + mp_dispatch_append(queue, item); +} + +// Like mp_dispatch_enqueue(), but +void mp_dispatch_enqueue_notify(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data) +{ + struct mp_dispatch_item *item = talloc_ptrtype(NULL, item); + *item = (struct mp_dispatch_item){ + .fn = fn, + .fn_data = fn_data, + .mergeable = true, + .asynchronous = true, + }; + mp_dispatch_append(queue, item); +} + +// Remove already queued item. Only items enqueued with the following functions +// can be canceled: +// - mp_dispatch_enqueue() +// - mp_dispatch_enqueue_notify() +// Items which were enqueued, and which are currently executing, can not be +// canceled anymore. This function is mostly for being called from the same +// context as mp_dispatch_queue_process(), where the "currently executing" case +// can be excluded. +void mp_dispatch_cancel_fn(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data) +{ + mp_mutex_lock(&queue->lock); + struct mp_dispatch_item **pcur = &queue->head; + queue->tail = NULL; + while (*pcur) { + struct mp_dispatch_item *cur = *pcur; + if (cur->fn == fn && cur->fn_data == fn_data) { + *pcur = cur->next; + talloc_free(cur); + } else { + queue->tail = cur; + pcur = &cur->next; + } + } + mp_mutex_unlock(&queue->lock); +} + +// Run fn(fn_data) on the target thread synchronously. This function enqueues +// the callback and waits until the target thread is done doing this. +// This is redundant to calling the function inside mp_dispatch_[un]lock(), +// but can be helpful with code that relies on TLS (such as OpenGL). +void mp_dispatch_run(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data) +{ + struct mp_dispatch_item item = { + .fn = fn, + .fn_data = fn_data, + }; + mp_dispatch_append(queue, &item); + + mp_mutex_lock(&queue->lock); + while (!item.completed) + mp_cond_wait(&queue->cond, &queue->lock); + mp_mutex_unlock(&queue->lock); +} + +// Process any outstanding dispatch items in the queue. This also handles +// suspending or locking the this thread from another thread via +// mp_dispatch_lock(). +// The timeout specifies the minimum wait time. The actual time spent in this +// function can be much higher if the suspending/locking functions are used, or +// if executing the dispatch items takes time. On the other hand, this function +// can return much earlier than the timeout due to sporadic wakeups. +// Note that this will strictly return only after: +// - timeout has passed, +// - all queue items were processed, +// - the possibly acquired lock has been released +// It's possible to cancel the timeout by calling mp_dispatch_interrupt(). +// Reentrant calls are not allowed. There can be only 1 thread calling +// mp_dispatch_queue_process() at a time. In addition, mp_dispatch_lock() can +// not be called from a thread that is calling mp_dispatch_queue_process() (i.e. +// no enqueued callback can call the lock/unlock functions). +void mp_dispatch_queue_process(struct mp_dispatch_queue *queue, double timeout) +{ + mp_mutex_lock(&queue->lock); + queue->wait = timeout > 0 ? mp_time_ns_add(mp_time_ns(), timeout) : 0; + assert(!queue->in_process); // recursion not allowed + queue->in_process = true; + queue->in_process_thread_id = mp_thread_current_id(); + // Wake up thread which called mp_dispatch_lock(). + if (queue->lock_requests) + mp_cond_broadcast(&queue->cond); + while (1) { + if (queue->lock_requests) { + // Block due to something having called mp_dispatch_lock(). + mp_cond_wait(&queue->cond, &queue->lock); + } else if (queue->head) { + struct mp_dispatch_item *item = queue->head; + queue->head = item->next; + if (!queue->head) + queue->tail = NULL; + item->next = NULL; + // Unlock, because we want to allow other threads to queue items + // while the dispatch item is processed. + // At the same time, we must prevent other threads from returning + // from mp_dispatch_lock(), which is done by locked=true. + assert(!queue->locked); + queue->locked = true; + mp_mutex_unlock(&queue->lock); + + item->fn(item->fn_data); + + mp_mutex_lock(&queue->lock); + assert(queue->locked); + queue->locked = false; + // Wakeup mp_dispatch_run(), also mp_dispatch_lock(). + mp_cond_broadcast(&queue->cond); + if (item->asynchronous) { + talloc_free(item); + } else { + item->completed = true; + } + } else if (queue->wait > 0 && !queue->interrupted) { + if (mp_cond_timedwait_until(&queue->cond, &queue->lock, queue->wait)) + queue->wait = 0; + } else { + break; + } + } + assert(!queue->locked); + queue->in_process = false; + queue->interrupted = false; + mp_mutex_unlock(&queue->lock); +} + +// If the queue is inside of mp_dispatch_queue_process(), make it return as +// soon as all work items have been run, without waiting for the timeout. This +// does not make it return early if it's blocked by a mp_dispatch_lock(). +// If the queue is _not_ inside of mp_dispatch_queue_process(), make the next +// call of it use a timeout of 0 (this is useful behavior if you need to +// wakeup the main thread from another thread in a race free way). +void mp_dispatch_interrupt(struct mp_dispatch_queue *queue) +{ + mp_mutex_lock(&queue->lock); + queue->interrupted = true; + mp_cond_broadcast(&queue->cond); + mp_mutex_unlock(&queue->lock); +} + +// If a mp_dispatch_queue_process() call is in progress, then adjust the maximum +// time it blocks due to its timeout argument. Otherwise does nothing. (It +// makes sense to call this in code that uses both mp_dispatch_[un]lock() and +// a normal event loop.) +// Does not work correctly with queues that have mp_dispatch_set_wakeup_fn() +// called on them, because this implies you actually do waiting via +// mp_dispatch_queue_process(), while wakeup callbacks are used when you need +// to wait in external APIs. +void mp_dispatch_adjust_timeout(struct mp_dispatch_queue *queue, int64_t until) +{ + mp_mutex_lock(&queue->lock); + if (queue->in_process && queue->wait > until) { + queue->wait = until; + mp_cond_broadcast(&queue->cond); + } + mp_mutex_unlock(&queue->lock); +} + +// Grant exclusive access to the target thread's state. While this is active, +// no other thread can return from mp_dispatch_lock() (i.e. it behaves like +// a pthread mutex), and no other thread can get dispatch items completed. +// Other threads can still queue asynchronous dispatch items without waiting, +// and the mutex behavior applies to this function and dispatch callbacks only. +// The lock is non-recursive, and dispatch callback functions can be thought of +// already holding the dispatch lock. +void mp_dispatch_lock(struct mp_dispatch_queue *queue) +{ + mp_mutex_lock(&queue->lock); + // Must not be called recursively from dispatched callbacks. + if (queue->in_process) + assert(!mp_thread_id_equal(queue->in_process_thread_id, mp_thread_current_id())); + // Must not be called recursively at all. + if (queue->locked_explicit) + assert(!mp_thread_id_equal(queue->locked_explicit_thread_id, mp_thread_current_id())); + queue->lock_requests += 1; + // And now wait until the target thread gets "trapped" within the + // mp_dispatch_queue_process() call, which will mean we get exclusive + // access to the target's thread state. + if (queue->onlock_fn) + queue->onlock_fn(queue->onlock_ctx); + while (!queue->in_process) { + mp_mutex_unlock(&queue->lock); + if (queue->wakeup_fn) + queue->wakeup_fn(queue->wakeup_ctx); + mp_mutex_lock(&queue->lock); + if (queue->in_process) + break; + mp_cond_wait(&queue->cond, &queue->lock); + } + // Wait until we can get the lock. + while (!queue->in_process || queue->locked) + mp_cond_wait(&queue->cond, &queue->lock); + // "Lock". + assert(queue->lock_requests); + assert(!queue->locked); + assert(!queue->locked_explicit); + queue->locked = true; + queue->locked_explicit = true; + queue->locked_explicit_thread_id = mp_thread_current_id(); + mp_mutex_unlock(&queue->lock); +} + +// Undo mp_dispatch_lock(). +void mp_dispatch_unlock(struct mp_dispatch_queue *queue) +{ + mp_mutex_lock(&queue->lock); + assert(queue->locked); + // Must be called after a mp_dispatch_lock(), from the same thread. + assert(queue->locked_explicit); + assert(mp_thread_id_equal(queue->locked_explicit_thread_id, mp_thread_current_id())); + // "Unlock". + queue->locked = false; + queue->locked_explicit = false; + queue->lock_requests -= 1; + // Wakeup mp_dispatch_queue_process(), and maybe other mp_dispatch_lock()s. + // (Would be nice to wake up only 1 other locker if lock_requests>0.) + mp_cond_broadcast(&queue->cond); + mp_mutex_unlock(&queue->lock); +} diff --git a/misc/dispatch.h b/misc/dispatch.h new file mode 100644 index 0000000..fbf8260 --- /dev/null +++ b/misc/dispatch.h @@ -0,0 +1,32 @@ +#ifndef MP_DISPATCH_H_ +#define MP_DISPATCH_H_ + +#include <stdint.h> + +typedef void (*mp_dispatch_fn)(void *data); +struct mp_dispatch_queue; + +struct mp_dispatch_queue *mp_dispatch_create(void *talloc_parent); +void mp_dispatch_set_wakeup_fn(struct mp_dispatch_queue *queue, + void (*wakeup_fn)(void *wakeup_ctx), + void *wakeup_ctx); +void mp_dispatch_set_onlock_fn(struct mp_dispatch_queue *queue, + void (*onlock_fn)(void *onlock_ctx), + void *onlock_ctx); +void mp_dispatch_enqueue(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data); +void mp_dispatch_enqueue_autofree(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data); +void mp_dispatch_enqueue_notify(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data); +void mp_dispatch_cancel_fn(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data); +void mp_dispatch_run(struct mp_dispatch_queue *queue, + mp_dispatch_fn fn, void *fn_data); +void mp_dispatch_queue_process(struct mp_dispatch_queue *queue, double timeout); +void mp_dispatch_interrupt(struct mp_dispatch_queue *queue); +void mp_dispatch_adjust_timeout(struct mp_dispatch_queue *queue, int64_t until); +void mp_dispatch_lock(struct mp_dispatch_queue *queue); +void mp_dispatch_unlock(struct mp_dispatch_queue *queue); + +#endif diff --git a/misc/jni.c b/misc/jni.c new file mode 100644 index 0000000..82f6356 --- /dev/null +++ b/misc/jni.c @@ -0,0 +1,429 @@ +/* + * JNI utility functions + * + * Copyright (c) 2015-2016 Matthieu Bouron <matthieu.bouron stupeflix.com> + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <libavcodec/jni.h> +#include <libavutil/mem.h> +#include <libavutil/bprint.h> +#include <stdlib.h> + +#include "jni.h" +#include "osdep/threads.h" + +static JavaVM *java_vm; +static pthread_key_t current_env; +static mp_once once = MP_STATIC_ONCE_INITIALIZER; +static mp_static_mutex lock = MP_STATIC_MUTEX_INITIALIZER; + +static void jni_detach_env(void *data) +{ + if (java_vm) { + (*java_vm)->DetachCurrentThread(java_vm); + } +} + +static void jni_create_pthread_key(void) +{ + pthread_key_create(¤t_env, jni_detach_env); +} + +JNIEnv *mp_jni_get_env(struct mp_log *log) +{ + int ret = 0; + JNIEnv *env = NULL; + + mp_mutex_lock(&lock); + if (java_vm == NULL) { + java_vm = av_jni_get_java_vm(NULL); + } + + if (!java_vm) { + mp_err(log, "No Java virtual machine has been registered\n"); + goto done; + } + + mp_exec_once(&once, jni_create_pthread_key); + + if ((env = pthread_getspecific(current_env)) != NULL) { + goto done; + } + + ret = (*java_vm)->GetEnv(java_vm, (void **)&env, JNI_VERSION_1_6); + switch(ret) { + case JNI_EDETACHED: + if ((*java_vm)->AttachCurrentThread(java_vm, &env, NULL) != 0) { + mp_err(log, "Failed to attach the JNI environment to the current thread\n"); + env = NULL; + } else { + pthread_setspecific(current_env, env); + } + break; + case JNI_OK: + break; + case JNI_EVERSION: + mp_err(log, "The specified JNI version is not supported\n"); + break; + default: + mp_err(log, "Failed to get the JNI environment attached to this thread\n"); + break; + } + +done: + mp_mutex_unlock(&lock); + return env; +} + +char *mp_jni_jstring_to_utf_chars(JNIEnv *env, jstring string, struct mp_log *log) +{ + char *ret = NULL; + const char *utf_chars = NULL; + + jboolean copy = 0; + + if (!string) { + return NULL; + } + + utf_chars = (*env)->GetStringUTFChars(env, string, ©); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + mp_err(log, "String.getStringUTFChars() threw an exception\n"); + return NULL; + } + + ret = av_strdup(utf_chars); + + (*env)->ReleaseStringUTFChars(env, string, utf_chars); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + mp_err(log, "String.releaseStringUTFChars() threw an exception\n"); + return NULL; + } + + return ret; +} + +jstring mp_jni_utf_chars_to_jstring(JNIEnv *env, const char *utf_chars, struct mp_log *log) +{ + jstring ret; + + ret = (*env)->NewStringUTF(env, utf_chars); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + mp_err(log, "NewStringUTF() threw an exception\n"); + return NULL; + } + + return ret; +} + +int mp_jni_exception_get_summary(JNIEnv *env, jthrowable exception, char **error, struct mp_log *log) +{ + int ret = 0; + + AVBPrint bp; + + char *name = NULL; + char *message = NULL; + + jclass class_class = NULL; + jmethodID get_name_id = NULL; + + jclass exception_class = NULL; + jmethodID get_message_id = NULL; + + jstring string = NULL; + + av_bprint_init(&bp, 0, AV_BPRINT_SIZE_AUTOMATIC); + + exception_class = (*env)->GetObjectClass(env, exception); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + mp_err(log, "Could not find Throwable class\n"); + ret = -1; + goto done; + } + + class_class = (*env)->GetObjectClass(env, exception_class); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + mp_err(log, "Could not find Throwable class's class\n"); + ret = -1; + goto done; + } + + get_name_id = (*env)->GetMethodID(env, class_class, "getName", "()Ljava/lang/String;"); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + mp_err(log, "Could not find method Class.getName()\n"); + ret = -1; + goto done; + } + + string = (*env)->CallObjectMethod(env, exception_class, get_name_id); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + mp_err(log, "Class.getName() threw an exception\n"); + ret = -1; + goto done; + } + + if (string) { + name = mp_jni_jstring_to_utf_chars(env, string, log); + (*env)->DeleteLocalRef(env, string); + string = NULL; + } + + get_message_id = (*env)->GetMethodID(env, exception_class, "getMessage", "()Ljava/lang/String;"); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + mp_err(log, "Could not find method java/lang/Throwable.getMessage()\n"); + ret = -1; + goto done; + } + + string = (*env)->CallObjectMethod(env, exception, get_message_id); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + mp_err(log, "Throwable.getMessage() threw an exception\n"); + ret = -1; + goto done; + } + + if (string) { + message = mp_jni_jstring_to_utf_chars(env, string, log); + (*env)->DeleteLocalRef(env, string); + string = NULL; + } + + if (name && message) { + av_bprintf(&bp, "%s: %s", name, message); + } else if (name && !message) { + av_bprintf(&bp, "%s occurred", name); + } else if (!name && message) { + av_bprintf(&bp, "Exception: %s", message); + } else { + mp_warn(log, "Could not retrieve exception name and message\n"); + av_bprintf(&bp, "Exception occurred"); + } + + ret = av_bprint_finalize(&bp, error); +done: + + av_free(name); + av_free(message); + + if (class_class) { + (*env)->DeleteLocalRef(env, class_class); + } + + if (exception_class) { + (*env)->DeleteLocalRef(env, exception_class); + } + + if (string) { + (*env)->DeleteLocalRef(env, string); + } + + return ret; +} + +int mp_jni_exception_check(JNIEnv *env, int logging, struct mp_log *log) +{ + int ret; + + jthrowable exception; + + char *message = NULL; + + if (!(*(env))->ExceptionCheck((env))) { + return 0; + } + + if (!logging) { + (*(env))->ExceptionClear((env)); + return -1; + } + + exception = (*env)->ExceptionOccurred(env); + (*(env))->ExceptionClear((env)); + + if ((ret = mp_jni_exception_get_summary(env, exception, &message, log)) < 0) { + (*env)->DeleteLocalRef(env, exception); + return ret; + } + + (*env)->DeleteLocalRef(env, exception); + + mp_err(log, "%s\n", message); + av_free(message); + + return -1; +} + +int mp_jni_init_jfields(JNIEnv *env, void *jfields, const struct MPJniField *jfields_mapping, int global, struct mp_log *log) +{ + int i, ret = 0; + jclass last_clazz = NULL; + + for (i = 0; jfields_mapping[i].name; i++) { + int mandatory = jfields_mapping[i].mandatory; + enum MPJniFieldType type = jfields_mapping[i].type; + + if (type == MP_JNI_CLASS) { + jclass clazz; + + last_clazz = NULL; + + clazz = (*env)->FindClass(env, jfields_mapping[i].name); + if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) { + goto done; + } + + last_clazz = *(jclass*)((uint8_t*)jfields + jfields_mapping[i].offset) = + global ? (*env)->NewGlobalRef(env, clazz) : clazz; + + if (global) { + (*env)->DeleteLocalRef(env, clazz); + } + + } else { + + if (!last_clazz) { + ret = -1; + break; + } + + switch(type) { + case MP_JNI_FIELD: { + jfieldID field_id = (*env)->GetFieldID(env, last_clazz, jfields_mapping[i].method, jfields_mapping[i].signature); + if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) { + goto done; + } + + *(jfieldID*)((uint8_t*)jfields + jfields_mapping[i].offset) = field_id; + break; + } + case MP_JNI_STATIC_FIELD_AS_INT: + case MP_JNI_STATIC_FIELD: { + jfieldID field_id = (*env)->GetStaticFieldID(env, last_clazz, jfields_mapping[i].method, jfields_mapping[i].signature); + if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) { + goto done; + } + + if (type == MP_JNI_STATIC_FIELD_AS_INT) { + if (field_id) { + jint value = (*env)->GetStaticIntField(env, last_clazz, field_id); + if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) { + goto done; + } + *(jint*)((uint8_t*)jfields + jfields_mapping[i].offset) = value; + } + } else { + *(jfieldID*)((uint8_t*)jfields + jfields_mapping[i].offset) = field_id; + } + break; + } + case MP_JNI_METHOD: { + jmethodID method_id = (*env)->GetMethodID(env, last_clazz, jfields_mapping[i].method, jfields_mapping[i].signature); + if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) { + goto done; + } + + *(jmethodID*)((uint8_t*)jfields + jfields_mapping[i].offset) = method_id; + break; + } + case MP_JNI_STATIC_METHOD: { + jmethodID method_id = (*env)->GetStaticMethodID(env, last_clazz, jfields_mapping[i].method, jfields_mapping[i].signature); + if ((ret = mp_jni_exception_check(env, mandatory, log)) < 0 && mandatory) { + goto done; + } + + *(jmethodID*)((uint8_t*)jfields + jfields_mapping[i].offset) = method_id; + break; + } + default: + mp_err(log, "Unknown JNI field type\n"); + ret = -1; + goto done; + } + + ret = 0; + } + } + +done: + if (ret < 0) { + /* reset jfields in case of failure so it does not leak references */ + mp_jni_reset_jfields(env, jfields, jfields_mapping, global, log); + } + + return ret; +} + +int mp_jni_reset_jfields(JNIEnv *env, void *jfields, const struct MPJniField *jfields_mapping, int global, struct mp_log *log) +{ + int i; + + for (i = 0; jfields_mapping[i].name; i++) { + enum MPJniFieldType type = jfields_mapping[i].type; + + switch(type) { + case MP_JNI_CLASS: { + jclass clazz = *(jclass*)((uint8_t*)jfields + jfields_mapping[i].offset); + if (!clazz) + continue; + + if (global) { + (*env)->DeleteGlobalRef(env, clazz); + } else { + (*env)->DeleteLocalRef(env, clazz); + } + + *(jclass*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL; + break; + } + case MP_JNI_FIELD: { + *(jfieldID*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL; + break; + } + case MP_JNI_STATIC_FIELD: { + *(jfieldID*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL; + break; + } + case MP_JNI_STATIC_FIELD_AS_INT: { + *(jint*)((uint8_t*)jfields + jfields_mapping[i].offset) = 0; + break; + } + case MP_JNI_METHOD: { + *(jmethodID*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL; + break; + } + case MP_JNI_STATIC_METHOD: { + *(jmethodID*)((uint8_t*)jfields + jfields_mapping[i].offset) = NULL; + break; + } + default: + mp_err(log, "Unknown JNI field type\n"); + } + } + + return 0; +} diff --git a/misc/jni.h b/misc/jni.h new file mode 100644 index 0000000..c9e4c28 --- /dev/null +++ b/misc/jni.h @@ -0,0 +1,161 @@ +/* + * JNI utility functions + * + * Copyright (c) 2015-2016 Matthieu Bouron <matthieu.bouron stupeflix.com> + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MP_JNI_H +#define MP_JNI_H + +#include <jni.h> +#include "common/msg.h" + +/* Convenience macros */ +#define MP_JNI_GET_ENV(obj) mp_jni_get_env((obj)->log) +#define MP_JNI_EXCEPTION_CHECK() mp_jni_exception_check(env, 0, NULL) +#define MP_JNI_EXCEPTION_LOG(obj) mp_jni_exception_check(env, 1, (obj)->log) +#define MP_JNI_DO(what, obj, method, ...) (*env)->what(env, obj, method, ##__VA_ARGS__) +#define MP_JNI_NEW(clazz, method, ...) MP_JNI_DO(NewObject, clazz, method, ##__VA_ARGS__) +#define MP_JNI_CALL_INT(obj, method, ...) MP_JNI_DO(CallIntMethod, obj, method, ##__VA_ARGS__) +#define MP_JNI_CALL_BOOL(obj, method, ...) MP_JNI_DO(CallBooleanMethod, obj, method, ##__VA_ARGS__) +#define MP_JNI_CALL_VOID(obj, method, ...) MP_JNI_DO(CallVoidMethod, obj, method, ##__VA_ARGS__) +#define MP_JNI_CALL_STATIC_INT(clazz, method, ...) MP_JNI_DO(CallStaticIntMethod, clazz, method, ##__VA_ARGS__) +#define MP_JNI_CALL_OBJECT(obj, method, ...) MP_JNI_DO(CallObjectMethod, obj, method, ##__VA_ARGS__) +#define MP_JNI_GET_INT(obj, field) MP_JNI_DO(GetIntField, obj, field) +#define MP_JNI_GET_LONG(obj, field) MP_JNI_DO(GetLongField, obj, field) +#define MP_JNI_GET_BOOL(obj, field) MP_JNI_DO(GetBoolField, obj, field) + +/* + * Attach permanently a JNI environment to the current thread and retrieve it. + * + * If successfully attached, the JNI environment will automatically be detached + * at thread destruction. + * + * @param attached pointer to an integer that will be set to 1 if the + * environment has been attached to the current thread or 0 if it is + * already attached. + * @param log context used for logging + * @return the JNI environment on success, NULL otherwise + */ +JNIEnv *mp_jni_get_env(struct mp_log *log); + +/* + * Convert a jstring to its utf characters equivalent. + * + * @param env JNI environment + * @param string Java string to convert + * @param log context used for logging + * @return a pointer to an array of unicode characters on success, NULL + * otherwise + */ +char *mp_jni_jstring_to_utf_chars(JNIEnv *env, jstring string, struct mp_log *log); + +/* + * Convert utf chars to its jstring equivalent. + * + * @param env JNI environment + * @param utf_chars a pointer to an array of unicode characters + * @param log context used for logging + * @return a Java string object on success, NULL otherwise + */ +jstring mp_jni_utf_chars_to_jstring(JNIEnv *env, const char *utf_chars, struct mp_log *log); + +/* + * Extract the error summary from a jthrowable in the form of "className: errorMessage" + * + * @param env JNI environment + * @param exception exception to get the summary from + * @param error address pointing to the error, the value is updated if a + * summary can be extracted + * @param log context used for logging + * @return 0 on success, < 0 otherwise + */ +int mp_jni_exception_get_summary(JNIEnv *env, jthrowable exception, char **error, struct mp_log *log); + +/* + * Check if an exception has occurred,log it using av_log and clear it. + * + * @param env JNI environment + * @param value used to enable logging if an exception has occurred, + * 0 disables logging, != 0 enables logging + * @param log context used for logging + */ +int mp_jni_exception_check(JNIEnv *env, int logging, struct mp_log *log); + +/* + * Jni field type. + */ +enum MPJniFieldType { + + MP_JNI_CLASS, + MP_JNI_FIELD, + MP_JNI_STATIC_FIELD, + MP_JNI_STATIC_FIELD_AS_INT, + MP_JNI_METHOD, + MP_JNI_STATIC_METHOD + +}; + +/* + * Jni field describing a class, a field or a method to be retrieved using + * the mp_jni_init_jfields method. + */ +struct MPJniField { + + const char *name; + const char *method; + const char *signature; + enum MPJniFieldType type; + int offset; + int mandatory; + +}; + +/* + * Retrieve class references, field ids and method ids to an arbitrary structure. + * + * @param env JNI environment + * @param jfields a pointer to an arbitrary structure where the different + * fields are declared and where the MPJNIField mapping table offsets are + * pointing to + * @param jfields_mapping null terminated array of MPJNIFields describing + * the class/field/method to be retrieved + * @param global make the classes references global. It is the caller + * responsibility to properly release global references. + * @param log_ctx context used for logging, can be NULL + * @return 0 on success, < 0 otherwise + */ +int mp_jni_init_jfields(JNIEnv *env, void *jfields, const struct MPJniField *jfields_mapping, int global, struct mp_log *log); + +/* + * Delete class references, field ids and method ids of an arbitrary structure. + * + * @param env JNI environment + * @param jfields a pointer to an arbitrary structure where the different + * fields are declared and where the MPJNIField mapping table offsets are + * pointing to + * @param jfields_mapping null terminated array of MPJNIFields describing + * the class/field/method to be deleted + * @param global treat the classes references as global and delete them + * accordingly + * @param log_ctx context used for logging, can be NULL + * @return 0 on success, < 0 otherwise + */ +int mp_jni_reset_jfields(JNIEnv *env, void *jfields, const struct MPJniField *jfields_mapping, int global, struct mp_log *log); + +#endif diff --git a/misc/json.c b/misc/json.c new file mode 100644 index 0000000..608cfad --- /dev/null +++ b/misc/json.c @@ -0,0 +1,359 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +/* JSON parser: + * + * Unlike standard JSON, \u escapes don't allow you to specify UTF-16 surrogate + * pairs. There may be some differences how numbers are parsed (this parser + * doesn't verify what's passed to strtod(), and also prefers parsing numbers + * as integers with stroll() if possible). + * + * It has some non-standard extensions which shouldn't conflict with JSON: + * - a list or object item can have a trailing "," + * - object syntax accepts "=" in addition of ":" + * - object keys can be unquoted, if they start with a character in [A-Za-z_] + * and contain only characters in [A-Za-z0-9_] + * - byte escapes with "\xAB" are allowed (with AB being a 2 digit hex number) + * + * Also see: http://tools.ietf.org/html/rfc8259 + * + * JSON writer: + * + * Doesn't insert whitespace. It's literally a waste of space. + * + * Can output invalid UTF-8, if input is invalid UTF-8. Consumers are supposed + * to deal with somehow: either by using byte-strings for JSON, or by running + * a "fixup" pass on the input data. The latter could for example change + * invalid UTF-8 sequences to replacement characters. + */ + +#include <stdlib.h> +#include <string.h> +#include <math.h> +#include <errno.h> +#include <inttypes.h> +#include <assert.h> + +#include "common/common.h" +#include "misc/bstr.h" +#include "misc/ctype.h" + +#include "json.h" + +static bool eat_c(char **s, char c) +{ + if (**s == c) { + *s += 1; + return true; + } + return false; +} + +static void eat_ws(char **src) +{ + while (1) { + char c = **src; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') + return; + *src += 1; + } +} + +void json_skip_whitespace(char **src) +{ + eat_ws(src); +} + +static int read_id(void *ta_parent, struct mpv_node *dst, char **src) +{ + char *start = *src; + if (!mp_isalpha(**src) && **src != '_') + return -1; + while (mp_isalnum(**src) || **src == '_') + *src += 1; + if (**src == ' ') { + **src = '\0'; // we're allowed to mutate it => can avoid the strndup + *src += 1; + } else { + start = talloc_strndup(ta_parent, start, *src - start); + } + dst->format = MPV_FORMAT_STRING; + dst->u.string = start; + return 0; +} + +static int read_str(void *ta_parent, struct mpv_node *dst, char **src) +{ + if (!eat_c(src, '"')) + return -1; // not a string + char *str = *src; + char *cur = str; + bool has_escapes = false; + while (cur[0] && cur[0] != '"') { + if (cur[0] == '\\') { + has_escapes = true; + // skip >\"< and >\\< (latter to handle >\\"< correctly) + if (cur[1] == '"' || cur[1] == '\\') + cur++; + } + cur++; + } + if (cur[0] != '"') + return -1; // invalid termination + // Mutate input string so we have a null-terminated string to the literal. + // This is a stupid micro-optimization, so we can avoid allocation. + cur[0] = '\0'; + *src = cur + 1; + if (has_escapes) { + bstr unescaped = {0}; + bstr r = bstr0(str); + if (!mp_append_escaped_string(ta_parent, &unescaped, &r)) + return -1; // broken escapes + str = unescaped.start; // the function guarantees null-termination + } + dst->format = MPV_FORMAT_STRING; + dst->u.string = str; + return 0; +} + +static int read_sub(void *ta_parent, struct mpv_node *dst, char **src, + int max_depth) +{ + bool is_arr = eat_c(src, '['); + bool is_obj = !is_arr && eat_c(src, '{'); + if (!is_arr && !is_obj) + return -1; // not an array or object + char term = is_obj ? '}' : ']'; + struct mpv_node_list *list = talloc_zero(ta_parent, struct mpv_node_list); + while (1) { + eat_ws(src); + if (eat_c(src, term)) + break; + if (list->num > 0 && !eat_c(src, ',')) + return -1; // missing ',' + eat_ws(src); + // non-standard extension: allow a trailing "," + if (eat_c(src, term)) + break; + if (is_obj) { + struct mpv_node keynode; + // non-standard extension: allow unquoted strings as keys + if (read_id(list, &keynode, src) < 0 && + read_str(list, &keynode, src) < 0) + return -1; // key is not a string + eat_ws(src); + // non-standard extension: allow "=" instead of ":" + if (!eat_c(src, ':') && !eat_c(src, '=')) + return -1; // ':' missing + eat_ws(src); + MP_TARRAY_GROW(list, list->keys, list->num); + list->keys[list->num] = keynode.u.string; + } + MP_TARRAY_GROW(list, list->values, list->num); + if (json_parse(ta_parent, &list->values[list->num], src, max_depth) < 0) + return -1; + list->num++; + } + dst->format = is_obj ? MPV_FORMAT_NODE_MAP : MPV_FORMAT_NODE_ARRAY; + dst->u.list = list; + return 0; +} + +/* Parse the string in *src as JSON, and write the result into *dst. + * max_depth limits the recursion and JSON tree depth. + * Warning: this overwrites the input string (what *src points to)! + * Returns: + * 0: success, *dst is valid, *src points to the end (the caller must check + * whether *src really terminates) + * -1: failure, *dst is invalid, there may be dead allocs under ta_parent + * (ta_free_children(ta_parent) is the only way to free them) + * The input string can be mutated in both cases. *dst might contain string + * elements, which point into the (mutated) input string. + */ +int json_parse(void *ta_parent, struct mpv_node *dst, char **src, int max_depth) +{ + max_depth -= 1; + if (max_depth < 0) + return -1; + + eat_ws(src); + + char c = **src; + if (!c) + return -1; // early EOF + if (c == 'n' && strncmp(*src, "null", 4) == 0) { + *src += 4; + dst->format = MPV_FORMAT_NONE; + return 0; + } else if (c == 't' && strncmp(*src, "true", 4) == 0) { + *src += 4; + dst->format = MPV_FORMAT_FLAG; + dst->u.flag = 1; + return 0; + } else if (c == 'f' && strncmp(*src, "false", 5) == 0) { + *src += 5; + dst->format = MPV_FORMAT_FLAG; + dst->u.flag = 0; + return 0; + } else if (c == '"') { + return read_str(ta_parent, dst, src); + } else if (c == '[' || c == '{') { + return read_sub(ta_parent, dst, src, max_depth); + } else if (c == '-' || (c >= '0' && c <= '9')) { + // The number could be either a float or an int. JSON doesn't make a + // difference, but the client API does. + char *nsrci = *src, *nsrcf = *src; + errno = 0; + long long int numi = strtoll(*src, &nsrci, 0); + if (errno) + nsrci = *src; + errno = 0; + double numf = strtod(*src, &nsrcf); + if (errno) + nsrcf = *src; + if (nsrci >= nsrcf) { + *src = nsrci; + dst->format = MPV_FORMAT_INT64; // long long is usually 64 bits + dst->u.int64 = numi; + return 0; + } + if (nsrcf > *src && isfinite(numf)) { + *src = nsrcf; + dst->format = MPV_FORMAT_DOUBLE; + dst->u.double_ = numf; + return 0; + } + return -1; + } + return -1; // character doesn't start a valid token +} + + +#define APPEND(b, s) bstr_xappend(NULL, (b), bstr0(s)) + +static const char special_escape[] = { + ['\b'] = 'b', + ['\f'] = 'f', + ['\n'] = 'n', + ['\r'] = 'r', + ['\t'] = 't', +}; + +static void write_json_str(bstr *b, unsigned char *str) +{ + APPEND(b, "\""); + while (1) { + unsigned char *cur = str; + while (cur[0] >= 32 && cur[0] != '"' && cur[0] != '\\') + cur++; + if (!cur[0]) + break; + bstr_xappend(NULL, b, (bstr){str, cur - str}); + if (cur[0] == '\"') { + bstr_xappend(NULL, b, (bstr){"\\\"", 2}); + } else if (cur[0] == '\\') { + bstr_xappend(NULL, b, (bstr){"\\\\", 2}); + } else if (cur[0] < sizeof(special_escape) && special_escape[cur[0]]) { + bstr_xappend_asprintf(NULL, b, "\\%c", special_escape[cur[0]]); + } else { + bstr_xappend_asprintf(NULL, b, "\\u%04x", (unsigned char)cur[0]); + } + str = cur + 1; + } + APPEND(b, str); + APPEND(b, "\""); +} + +static void add_indent(bstr *b, int indent) +{ + if (indent < 0) + return; + bstr_xappend(NULL, b, bstr0("\n")); + for (int n = 0; n < indent; n++) + bstr_xappend(NULL, b, bstr0(" ")); +} + +static int json_append(bstr *b, const struct mpv_node *src, int indent) +{ + switch (src->format) { + case MPV_FORMAT_NONE: + APPEND(b, "null"); + return 0; + case MPV_FORMAT_FLAG: + APPEND(b, src->u.flag ? "true" : "false"); + return 0; + case MPV_FORMAT_INT64: + bstr_xappend_asprintf(NULL, b, "%"PRId64, src->u.int64); + return 0; + case MPV_FORMAT_DOUBLE: { + const char *px = (isfinite(src->u.double_) || indent == 0) ? "" : "\""; + bstr_xappend_asprintf(NULL, b, "%s%f%s", px, src->u.double_, px); + return 0; + } + case MPV_FORMAT_STRING: + if (indent == 0) + APPEND(b, src->u.string); + else + write_json_str(b, src->u.string); + return 0; + case MPV_FORMAT_NODE_ARRAY: + case MPV_FORMAT_NODE_MAP: { + struct mpv_node_list *list = src->u.list; + bool is_obj = src->format == MPV_FORMAT_NODE_MAP; + APPEND(b, is_obj ? "{" : "["); + int next_indent = indent >= 0 ? indent + 1 : -1; + for (int n = 0; n < list->num; n++) { + if (n) + APPEND(b, ","); + add_indent(b, next_indent); + if (is_obj) { + write_json_str(b, list->keys[n]); + APPEND(b, ":"); + } + json_append(b, &list->values[n], next_indent); + } + add_indent(b, indent); + APPEND(b, is_obj ? "}" : "]"); + return 0; + } + } + return -1; // unknown format +} + +static int json_append_str(char **dst, struct mpv_node *src, int indent) +{ + bstr buffer = bstr0(*dst); + int r = json_append(&buffer, src, indent); + *dst = buffer.start; + return r; +} + +/* Write the contents of *src as JSON, and append the JSON string to *dst. + * This will use strlen() to determine the start offset, and ta_get_size() + * and ta_realloc() to extend the memory allocation of *dst. + * Returns: 0 on success, <0 on failure. + */ +int json_write(char **dst, struct mpv_node *src) +{ + return json_append_str(dst, src, -1); +} + +// Same as json_write(), but add whitespace to make it readable. +int json_write_pretty(char **dst, struct mpv_node *src) +{ + return json_append_str(dst, src, 0); +} diff --git a/misc/json.h b/misc/json.h new file mode 100644 index 0000000..b99fc36 --- /dev/null +++ b/misc/json.h @@ -0,0 +1,31 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MP_JSON_H +#define MP_JSON_H + +// We reuse mpv_node. +#include "libmpv/client.h" + +#define MAX_JSON_DEPTH 50 + +int json_parse(void *ta_parent, struct mpv_node *dst, char **src, int max_depth); +void json_skip_whitespace(char **src); +int json_write(char **s, struct mpv_node *src); +int json_write_pretty(char **s, struct mpv_node *src); + +#endif diff --git a/misc/language.c b/misc/language.c new file mode 100644 index 0000000..92857f7 --- /dev/null +++ b/misc/language.c @@ -0,0 +1,362 @@ +/* + * Language code utility functions + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "language.h" + +#include "common/common.h" +#include "osdep/strnlen.h" + +#include <stdbool.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> + +static const struct lang { + char match[4]; + char canonical[4]; +} langmap[] = { + {"aa", "aar"}, + {"ab", "abk"}, + {"ae", "ave"}, + {"af", "afr"}, + {"ak", "aka"}, + {"am", "amh"}, + {"an", "arg"}, + {"ar", "ara"}, + {"as", "asm"}, + {"av", "ava"}, + {"ay", "aym"}, + {"az", "aze"}, + {"ba", "bak"}, + {"be", "bel"}, + {"bg", "bul"}, + {"bh", "bih"}, + {"bi", "bis"}, + {"bm", "bam"}, + {"bn", "ben"}, + {"bo", "tib"}, + {"bod", "tib"}, + {"br", "bre"}, + {"bs", "bos"}, + {"ca", "cat"}, + {"ce", "che"}, + {"ces", "cze"}, + {"ch", "cha"}, + {"co", "cos"}, + {"cr", "cre"}, + {"cs", "cze"}, + {"cu", "chu"}, + {"cv", "chv"}, + {"cy", "wel"}, + {"cym", "wel"}, + {"da", "dan"}, + {"de", "ger"}, + {"deu", "ger"}, + {"dv", "div"}, + {"dz", "dzo"}, + {"ee", "ewe"}, + {"el", "gre"}, + {"ell", "gre"}, + {"en", "eng"}, + {"eo", "epo"}, + {"es", "spa"}, + {"et", "est"}, + {"eu", "baq"}, + {"eus", "baq"}, + {"fa", "per"}, + {"fas", "per"}, + {"ff", "ful"}, + {"fi", "fin"}, + {"fj", "fij"}, + {"fo", "fao"}, + {"fr", "fre"}, + {"fra", "fre"}, + {"fy", "fry"}, + {"ga", "gle"}, + {"gd", "gla"}, + {"gl", "glg"}, + {"gn", "grn"}, + {"gu", "guj"}, + {"gv", "glv"}, + {"ha", "hau"}, + {"he", "heb"}, + {"hi", "hin"}, + {"ho", "hmo"}, + {"hr", "hrv"}, + {"ht", "hat"}, + {"hu", "hun"}, + {"hy", "arm"}, + {"hye", "arm"}, + {"hz", "her"}, + {"ia", "ina"}, + {"id", "ind"}, + {"ie", "ile"}, + {"ig", "ibo"}, + {"ii", "iii"}, + {"ik", "ipk"}, + {"io", "ido"}, + {"is", "ice"}, + {"isl", "ice"}, + {"it", "ita"}, + {"iu", "iku"}, + {"ja", "jpn"}, + {"jv", "jav"}, + {"ka", "geo"}, + {"kat", "geo"}, + {"kg", "kon"}, + {"ki", "kik"}, + {"kj", "kua"}, + {"kk", "kaz"}, + {"kl", "kal"}, + {"km", "khm"}, + {"kn", "kan"}, + {"ko", "kor"}, + {"kr", "kau"}, + {"ks", "kas"}, + {"ku", "kur"}, + {"kv", "kom"}, + {"kw", "cor"}, + {"ky", "kir"}, + {"la", "lat"}, + {"lb", "ltz"}, + {"lg", "lug"}, + {"li", "lim"}, + {"ln", "lin"}, + {"lo", "lao"}, + {"lt", "lit"}, + {"lu", "lub"}, + {"lv", "lav"}, + {"mg", "mlg"}, + {"mh", "mah"}, + {"mi", "mao"}, + {"mk", "mac"}, + {"mkd", "mac"}, + {"ml", "mal"}, + {"mn", "mon"}, + {"mr", "mar"}, + {"mri", "mao"}, + {"ms", "may"}, + {"msa", "may"}, + {"mt", "mlt"}, + {"my", "bur"}, + {"mya", "bur"}, + {"na", "nau"}, + {"nb", "nob"}, + {"nd", "nde"}, + {"ne", "nep"}, + {"ng", "ndo"}, + {"nl", "dut"}, + {"nld", "dut"}, + {"nn", "nno"}, + {"no", "nor"}, + {"nr", "nbl"}, + {"nv", "nav"}, + {"ny", "nya"}, + {"oc", "oci"}, + {"oj", "oji"}, + {"om", "orm"}, + {"or", "ori"}, + {"os", "oss"}, + {"pa", "pan"}, + {"pi", "pli"}, + {"pl", "pol"}, + {"ps", "pus"}, + {"pt", "por"}, + {"qu", "que"}, + {"rm", "roh"}, + {"rn", "run"}, + {"ro", "rum"}, + {"ron", "rum"}, + {"ru", "rus"}, + {"rw", "kin"}, + {"sa", "san"}, + {"sc", "srd"}, + {"sd", "snd"}, + {"se", "sme"}, + {"sg", "sag"}, + {"si", "sin"}, + {"sk", "slo"}, + {"sl", "slv"}, + {"slk", "slo"}, + {"sm", "smo"}, + {"sn", "sna"}, + {"so", "som"}, + {"sq", "alb"}, + {"sqi", "alb"}, + {"sr", "srp"}, + {"ss", "ssw"}, + {"st", "sot"}, + {"su", "sun"}, + {"sv", "swe"}, + {"sw", "swa"}, + {"ta", "tam"}, + {"te", "tel"}, + {"tg", "tgk"}, + {"th", "tha"}, + {"ti", "tir"}, + {"tk", "tuk"}, + {"tl", "tgl"}, + {"tn", "tsn"}, + {"to", "ton"}, + {"tr", "tur"}, + {"ts", "tso"}, + {"tt", "tat"}, + {"tw", "twi"}, + {"ty", "tah"}, + {"ug", "uig"}, + {"uk", "ukr"}, + {"ur", "urd"}, + {"uz", "uzb"}, + {"ve", "ven"}, + {"vi", "vie"}, + {"vo", "vol"}, + {"wa", "wln"}, + {"wo", "wol"}, + {"xh", "xho"}, + {"yi", "yid"}, + {"yo", "yor"}, + {"za", "zha"}, + {"zh", "chi"}, + {"zho", "chi"}, + {"zu", "zul"}, +}; + +struct langsearch { + const char *str; + size_t size; +}; + +static int lang_compare(const void *s, const void *k) +{ + const struct langsearch *search = s; + const struct lang *key = k; + + int ret = strncasecmp(search->str, key->match, search->size); + if (!ret && search->size < sizeof(key->match) && key->match[search->size]) + return 1; + return ret; +} + +static void canonicalize(const char **lang, size_t *size) +{ + if (*size > sizeof(langmap[0].match)) + return; + + struct langsearch search = {*lang, *size}; + struct lang *l = bsearch(&search, langmap, MP_ARRAY_SIZE(langmap), sizeof(langmap[0]), + &lang_compare); + + if (l) { + *lang = l->canonical; + *size = strnlen(l->canonical, sizeof(l->canonical)); + } +} + +static bool tag_matches(const char *l1, size_t s1, const char *l2, size_t s2) +{ + return s1 == s2 && !strncasecmp(l1, l2, s1); +} + +int mp_match_lang_single(const char *l1, const char *l2) +{ + // We never consider null or empty strings to match + if (!l1 || !l2 || !*l1 || !*l2) + return 0; + + // The first subtag should always be a language; canonicalize to 3-letter ISO 639-2B (arbitrarily chosen) + size_t s1 = strcspn(l1, "-_"); + size_t s2 = strcspn(l2, "-_"); + + const char *l1c = l1; + const char *l2c = l2; + size_t s1c = s1; + size_t s2c = s2; + + canonicalize(&l1c, &s1c); + canonicalize(&l2c, &s2c); + + // If the first subtags don't match, we have no match at all + if (!tag_matches(l1c, s1c, l2c, s2c)) + return 0; + + // Attempt to match each subtag in each string against each in the other + int score = 1; + bool x1 = false; + int count = 0; + for (;;) { + l1 += s1; + + while (*l1 == '-' || *l1 == '_') + l1++; + + if (!*l1) + break; + + s1 = strcspn(l1, "-_"); + if (tag_matches(l1, s1, "x", 1)) { + x1 = true; + continue; + } + + const char *l2o = l2; + size_t s2o = s2; + bool x2 = false; + for (;;) { + l2 += s2; + + while (*l2 == '-' || *l2 == '_') + l2++; + + if (!*l2) + break; + + s2 = strcspn(l2, "-_"); + if (tag_matches(l2, s2, "x", 1)) { + x2 = true; + if (!x1) + break; + continue; + } + + // Private-use subtags only match against other private-use subtags + if (x1 && !x2) + continue; + + if (tag_matches(l1c, s1c, l2c, s2c)) { + // Matches for subtags earlier in the user's string take priority over later ones, + // for up to LANGUAGE_SCORE_BITS subtags + int shift = (LANGUAGE_SCORE_BITS - count - 1); + if (shift < 0) + shift = 0; + score += (1 << shift); + + if (score >= LANGUAGE_SCORE_MAX) + return LANGUAGE_SCORE_MAX; + } + } + + l2 = l2o; + s2 = s2o; + + count++; + } + + return score; +} diff --git a/misc/language.h b/misc/language.h new file mode 100644 index 0000000..250d391 --- /dev/null +++ b/misc/language.h @@ -0,0 +1,31 @@ +/* + * Language code utility functions + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MP_LANGUAGE_H +#define MP_LANGUAGE_H + +#define LANGUAGE_SCORE_BITS 16 +#define LANGUAGE_SCORE_MAX (1 << LANGUAGE_SCORE_BITS) + +// Where applicable, l1 is the user-specified code and l2 is the code being checked against it +int mp_match_lang_single(const char *l1, const char *l2); + +char **mp_get_user_langs(void); + +#endif /* MP_LANGUAGE_H */ diff --git a/misc/linked_list.h b/misc/linked_list.h new file mode 100644 index 0000000..b43b227 --- /dev/null +++ b/misc/linked_list.h @@ -0,0 +1,107 @@ +#pragma once + +#include <stddef.h> + +/* + * Doubly linked list macros. All of these require that each list item is a + * struct, that contains a field, that is another struct with prev/next fields: + * + * struct example_item { + * struct { + * struct example_item *prev, *next; + * } mylist; + * }; + * + * And a struct somewhere that represents the "list" and has head/tail fields: + * + * struct { + * struct example_item *head, *tail; + * } mylist_var; + * + * Then you can e.g. insert elements like this: + * + * struct example_item item; + * LL_APPEND(mylist, &mylist_var, &item); + * + * The first macro argument is always the name if the field in the item that + * contains the prev/next pointers, in this case struct example_item.mylist. + * This was done so that a single item can be in multiple lists. + * + * The list is started/terminated with NULL. Nothing ever points _to_ the + * list head, so the list head memory location can be safely moved. + * + * General rules are: + * - list head is initialized by setting head/tail to NULL + * - list items do not need to be initialized before inserting them + * - next/prev fields of list items are not cleared when they are removed + * - there's no way to know whether an item is in the list or not (unless + * you clear prev/next on init/removal, _and_ check whether items with + * prev/next==NULL are referenced by head/tail) + */ + +// Insert item at the end of the list (list->tail == item). +// Undefined behavior if item is already in the list. +#define LL_APPEND(field, list, item) do { \ + (item)->field.prev = (list)->tail; \ + (item)->field.next = NULL; \ + LL_RELINK_(field, list, item) \ +} while (0) + +// Insert item enew after eprev (i.e. eprev->next == enew). If eprev is NULL, +// then insert it as head (list->head == enew). +// Undefined behavior if enew is already in the list, or eprev isn't. +#define LL_INSERT_AFTER(field, list, eprev, enew) do { \ + (enew)->field.prev = (eprev); \ + (enew)->field.next = (eprev) ? (eprev)->field.next : (list)->head; \ + LL_RELINK_(field, list, enew) \ +} while (0) + +// Insert item at the start of the list (list->head == item). +// Undefined behavior if item is already in the list. +#define LL_PREPEND(field, list, item) do { \ + (item)->field.prev = NULL; \ + (item)->field.next = (list)->head; \ + LL_RELINK_(field, list, item) \ +} while (0) + +// Insert item enew before enext (i.e. enew->next == enext). If enext is NULL, +// then insert it as tail (list->tail == enew). +// Undefined behavior if enew is already in the list, or enext isn't. +#define LL_INSERT_BEFORE(field, list, enext, enew) do { \ + (enew)->field.prev = (enext) ? (enext)->field.prev : (list)->tail; \ + (enew)->field.next = (enext); \ + LL_RELINK_(field, list, enew) \ +} while (0) + +// Remove the item from the list. +// Undefined behavior if item is not in the list. +#define LL_REMOVE(field, list, item) do { \ + if ((item)->field.prev) { \ + (item)->field.prev->field.next = (item)->field.next; \ + } else { \ + (list)->head = (item)->field.next; \ + } \ + if ((item)->field.next) { \ + (item)->field.next->field.prev = (item)->field.prev; \ + } else { \ + (list)->tail = (item)->field.prev; \ + } \ +} while (0) + +// Remove all items from the list. +#define LL_CLEAR(field, list) do { \ + (list)->head = (list)->tail = NULL; \ +} while (0) + +// Internal helper. +#define LL_RELINK_(field, list, item) \ + if ((item)->field.prev) { \ + (item)->field.prev->field.next = (item); \ + } else { \ + (list)->head = (item); \ + } \ + if ((item)->field.next) { \ + (item)->field.next->field.prev = (item); \ + } else { \ + (list)->tail = (item); \ + } diff --git a/misc/natural_sort.c b/misc/natural_sort.c new file mode 100644 index 0000000..3e0bab0 --- /dev/null +++ b/misc/natural_sort.c @@ -0,0 +1,67 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "misc/ctype.h" + +#include "natural_sort.h" + +// Comparison function for an ASCII-only "natural" sort. Case is ignored and +// numbers are ordered by value regardless of padding. Two filenames that differ +// only in the padding of numbers will be considered equal and end up in +// arbitrary order. Bytes outside of A-Z/a-z/0-9 will by sorted by byte value. +int mp_natural_sort_cmp(const char *name1, const char *name2) +{ + while (name1[0] && name2[0]) { + if (mp_isdigit(name1[0]) && mp_isdigit(name2[0])) { + while (name1[0] == '0') + name1++; + while (name2[0] == '0') + name2++; + const char *end1 = name1, *end2 = name2; + while (mp_isdigit(*end1)) + end1++; + while (mp_isdigit(*end2)) + end2++; + // With padding stripped, a number with more digits is bigger. + if ((end1 - name1) < (end2 - name2)) + return -1; + if ((end1 - name1) > (end2 - name2)) + return 1; + // Same length, lexicographical works. + while (name1 < end1) { + if (name1[0] < name2[0]) + return -1; + if (name1[0] > name2[0]) + return 1; + name1++; + name2++; + } + } else { + if (mp_tolower(name1[0]) < mp_tolower(name2[0])) + return -1; + if (mp_tolower(name1[0]) > mp_tolower(name2[0])) + return 1; + name1++; + name2++; + } + } + if (name2[0]) + return -1; + if (name1[0]) + return 1; + return 0; +} diff --git a/misc/natural_sort.h b/misc/natural_sort.h new file mode 100644 index 0000000..47b9a7a --- /dev/null +++ b/misc/natural_sort.h @@ -0,0 +1,23 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MP_NATURAL_SORT_H +#define MP_NATURAL_SORT_H + +int mp_natural_sort_cmp(const char *name1, const char *name2); + +#endif diff --git a/misc/node.c b/misc/node.c new file mode 100644 index 0000000..5bf3211 --- /dev/null +++ b/misc/node.c @@ -0,0 +1,159 @@ +#include "common/common.h" + +#include "node.h" + +// Init a node with the given format. If parent is not NULL, it is set as +// parent allocation according to m_option_type_node rules (which means +// the mpv_node_list allocs are used for chaining the TA allocations). +// format == MPV_FORMAT_NONE will simply initialize it with all-0. +void node_init(struct mpv_node *dst, int format, struct mpv_node *parent) +{ + // Other formats need to be initialized manually. + assert(format == MPV_FORMAT_NODE_MAP || format == MPV_FORMAT_NODE_ARRAY || + format == MPV_FORMAT_FLAG || format == MPV_FORMAT_INT64 || + format == MPV_FORMAT_DOUBLE || format == MPV_FORMAT_BYTE_ARRAY || + format == MPV_FORMAT_NONE); + + void *ta_parent = NULL; + if (parent) { + assert(parent->format == MPV_FORMAT_NODE_MAP || + parent->format == MPV_FORMAT_NODE_ARRAY); + ta_parent = parent->u.list; + } + + *dst = (struct mpv_node){ .format = format }; + if (format == MPV_FORMAT_NODE_MAP || format == MPV_FORMAT_NODE_ARRAY) + dst->u.list = talloc_zero(ta_parent, struct mpv_node_list); + if (format == MPV_FORMAT_BYTE_ARRAY) + dst->u.ba = talloc_zero(ta_parent, struct mpv_byte_array); +} + +// Add an entry to a MPV_FORMAT_NODE_ARRAY. +// m_option_type_node memory management rules apply. +struct mpv_node *node_array_add(struct mpv_node *dst, int format) +{ + struct mpv_node_list *list = dst->u.list; + assert(dst->format == MPV_FORMAT_NODE_ARRAY && dst->u.list); + MP_TARRAY_GROW(list, list->values, list->num); + node_init(&list->values[list->num], format, dst); + return &list->values[list->num++]; +} + +// Add an entry to a MPV_FORMAT_NODE_MAP. Keep in mind that this does +// not check for already existing entries under the same key. +// m_option_type_node memory management rules apply. +struct mpv_node *node_map_add(struct mpv_node *dst, const char *key, int format) +{ + assert(key); + return node_map_badd(dst, bstr0(key), format); +} + +struct mpv_node *node_map_badd(struct mpv_node *dst, struct bstr key, int format) +{ + assert(key.start); + + struct mpv_node_list *list = dst->u.list; + assert(dst->format == MPV_FORMAT_NODE_MAP && dst->u.list); + MP_TARRAY_GROW(list, list->values, list->num); + MP_TARRAY_GROW(list, list->keys, list->num); + list->keys[list->num] = bstrdup0(list, key); + node_init(&list->values[list->num], format, dst); + return &list->values[list->num++]; +} + +// Add a string entry to a MPV_FORMAT_NODE_MAP. Keep in mind that this does +// not check for already existing entries under the same key. +// m_option_type_node memory management rules apply. +void node_map_add_string(struct mpv_node *dst, const char *key, const char *val) +{ + assert(val); + + struct mpv_node *entry = node_map_add(dst, key, MPV_FORMAT_NONE); + entry->format = MPV_FORMAT_STRING; + entry->u.string = talloc_strdup(dst->u.list, val); +} + +void node_map_add_int64(struct mpv_node *dst, const char *key, int64_t v) +{ + node_map_add(dst, key, MPV_FORMAT_INT64)->u.int64 = v; +} + +void node_map_add_double(struct mpv_node *dst, const char *key, double v) +{ + node_map_add(dst, key, MPV_FORMAT_DOUBLE)->u.double_ = v; +} + +void node_map_add_flag(struct mpv_node *dst, const char *key, bool v) +{ + node_map_add(dst, key, MPV_FORMAT_FLAG)->u.flag = v; +} + +mpv_node *node_map_get(mpv_node *src, const char *key) +{ + return node_map_bget(src, bstr0(key)); +} + +mpv_node *node_map_bget(mpv_node *src, struct bstr key) +{ + if (src->format != MPV_FORMAT_NODE_MAP) + return NULL; + + for (int i = 0; i < src->u.list->num; i++) { + if (bstr_equals0(key, src->u.list->keys[i])) + return &src->u.list->values[i]; + } + + return NULL; +} + +// Note: for MPV_FORMAT_NODE_MAP, this (incorrectly) takes the order into +// account, instead of treating it as set. +bool equal_mpv_value(const void *a, const void *b, mpv_format format) +{ + switch (format) { + case MPV_FORMAT_NONE: + return true; + case MPV_FORMAT_STRING: + case MPV_FORMAT_OSD_STRING: + return strcmp(*(char **)a, *(char **)b) == 0; + case MPV_FORMAT_FLAG: + return *(int *)a == *(int *)b; + case MPV_FORMAT_INT64: + return *(int64_t *)a == *(int64_t *)b; + case MPV_FORMAT_DOUBLE: + return *(double *)a == *(double *)b; + case MPV_FORMAT_NODE: + return equal_mpv_node(a, b); + case MPV_FORMAT_BYTE_ARRAY: { + const struct mpv_byte_array *a_r = a, *b_r = b; + if (a_r->size != b_r->size) + return false; + return memcmp(a_r->data, b_r->data, a_r->size) == 0; + } + case MPV_FORMAT_NODE_ARRAY: + case MPV_FORMAT_NODE_MAP: + { + mpv_node_list *l_a = *(mpv_node_list **)a, *l_b = *(mpv_node_list **)b; + if (l_a->num != l_b->num) + return false; + for (int n = 0; n < l_a->num; n++) { + if (format == MPV_FORMAT_NODE_MAP) { + if (strcmp(l_a->keys[n], l_b->keys[n]) != 0) + return false; + } + if (!equal_mpv_node(&l_a->values[n], &l_b->values[n])) + return false; + } + return true; + } + } + MP_ASSERT_UNREACHABLE(); // supposed to be able to handle all defined types +} + +// Remarks see equal_mpv_value(). +bool equal_mpv_node(const struct mpv_node *a, const struct mpv_node *b) +{ + if (a->format != b->format) + return false; + return equal_mpv_value(&a->u, &b->u, a->format); +} diff --git a/misc/node.h b/misc/node.h new file mode 100644 index 0000000..688b0a8 --- /dev/null +++ b/misc/node.h @@ -0,0 +1,20 @@ +#ifndef MP_MISC_NODE_H_ +#define MP_MISC_NODE_H_ + +#include "libmpv/client.h" +#include "misc/bstr.h" + +void node_init(struct mpv_node *dst, int format, struct mpv_node *parent); +struct mpv_node *node_array_add(struct mpv_node *dst, int format); +struct mpv_node *node_map_add(struct mpv_node *dst, const char *key, int format); +struct mpv_node *node_map_badd(struct mpv_node *dst, struct bstr key, int format); +void node_map_add_string(struct mpv_node *dst, const char *key, const char *val); +void node_map_add_int64(struct mpv_node *dst, const char *key, int64_t v); +void node_map_add_double(struct mpv_node *dst, const char *key, double v); +void node_map_add_flag(struct mpv_node *dst, const char *key, bool v); +mpv_node *node_map_get(mpv_node *src, const char *key); +mpv_node *node_map_bget(mpv_node *src, struct bstr key); +bool equal_mpv_value(const void *a, const void *b, mpv_format format); +bool equal_mpv_node(const struct mpv_node *a, const struct mpv_node *b); + +#endif diff --git a/misc/random.c b/misc/random.c new file mode 100644 index 0000000..e622ab7 --- /dev/null +++ b/misc/random.c @@ -0,0 +1,75 @@ +/* + * Implementation of non-cryptographic pseudo-random number + * generator algorithm known as xoshiro. + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdint.h> + +#include "osdep/threads.h" +#include "random.h" + +static uint64_t state[4]; +static mp_static_mutex state_mutex = MP_STATIC_MUTEX_INITIALIZER; + +static inline uint64_t rotl_u64(const uint64_t x, const int k) +{ + return (x << k) | (x >> (64 - k)); +} + +static inline uint64_t splitmix64(uint64_t *const x) +{ + uint64_t z = (*x += UINT64_C(0x9e3779b97f4a7c15)); + z = (z ^ (z >> 30)) * UINT64_C(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)) * UINT64_C(0x94d049bb133111eb); + return z ^ (z >> 31); +} + +void mp_rand_seed(uint64_t seed) +{ + mp_mutex_lock(&state_mutex); + state[0] = seed; + for (int i = 1; i < 4; i++) + state[i] = splitmix64(&seed); + mp_mutex_unlock(&state_mutex); +} + +uint64_t mp_rand_next(void) +{ + uint64_t result, t; + + mp_mutex_lock(&state_mutex); + + result = rotl_u64(state[1] * 5, 7) * 9; + t = state[1] << 17; + + state[2] ^= state[0]; + state[3] ^= state[1]; + state[1] ^= state[2]; + state[0] ^= state[3]; + state[2] ^= t; + state[3] = rotl_u64(state[3], 45); + + mp_mutex_unlock(&state_mutex); + + return result; +} + +double mp_rand_next_double(void) +{ + return (mp_rand_next() >> 11) * 0x1.0p-53; +} diff --git a/misc/random.h b/misc/random.h new file mode 100644 index 0000000..dae66a0 --- /dev/null +++ b/misc/random.h @@ -0,0 +1,41 @@ +/* + * Implementation of non-cryptographic pseudo-random number + * generator algorithm known as xoshiro. + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> + +/* + * Initialize the pseudo-random number generator's state with + * the given 64-bit seed. + */ +void mp_rand_seed(uint64_t seed); + +/* + * Return the next 64-bit pseudo-random integer, and update the state + * accordingly. + */ +uint64_t mp_rand_next(void); + +/* + * Return a double value in the range of [0.0, 1.0) with uniform + * distribution, and update the state accordingly. + */ +double mp_rand_next_double(void); diff --git a/misc/rendezvous.c b/misc/rendezvous.c new file mode 100644 index 0000000..1fe5724 --- /dev/null +++ b/misc/rendezvous.c @@ -0,0 +1,55 @@ + +#include "rendezvous.h" + +#include "osdep/threads.h" + +static mp_static_mutex lock = MP_STATIC_MUTEX_INITIALIZER; +static mp_cond wakeup = MP_STATIC_COND_INITIALIZER; + +static struct waiter *waiters; + +struct waiter { + void *tag; + struct waiter *next; + intptr_t *value; +}; + +/* A barrier for 2 threads, which can exchange a value when they meet. + * The first thread to call this function will block. As soon as two threads + * are calling this function with the same tag value, they will unblock, and + * on each thread the call returns the value parameter of the _other_ thread. + * + * tag is an arbitrary value, but it must be an unique pointer. If there are + * more than 2 threads using the same tag, things won't work. Typically, it + * will have to point to a memory allocation or to the stack, while pointing + * it to static data is always a bug. + * + * This shouldn't be used for performance critical code (uses a linked list + * of _all_ waiters in the process, and temporarily wakes up _all_ waiters on + * each second call). + * + * This is inspired by: http://9atom.org/magic/man2html/2/rendezvous */ +intptr_t mp_rendezvous(void *tag, intptr_t value) +{ + struct waiter wait = { .tag = tag, .value = &value }; + mp_mutex_lock(&lock); + struct waiter **prev = &waiters; + while (*prev) { + if ((*prev)->tag == tag) { + intptr_t tmp = *(*prev)->value; + *(*prev)->value = value; + value = tmp; + (*prev)->value = NULL; // signals completion + *prev = (*prev)->next; // unlink + mp_cond_broadcast(&wakeup); + goto done; + } + prev = &(*prev)->next; + } + *prev = &wait; + while (wait.value) + mp_cond_wait(&wakeup, &lock); +done: + mp_mutex_unlock(&lock); + return value; +} diff --git a/misc/rendezvous.h b/misc/rendezvous.h new file mode 100644 index 0000000..ffcc89a --- /dev/null +++ b/misc/rendezvous.h @@ -0,0 +1,8 @@ +#ifndef MP_RENDEZVOUS_H_ +#define MP_RENDEZVOUS_H_ + +#include <stdint.h> + +intptr_t mp_rendezvous(void *tag, intptr_t value); + +#endif diff --git a/misc/thread_pool.c b/misc/thread_pool.c new file mode 100644 index 0000000..e20d9d0 --- /dev/null +++ b/misc/thread_pool.c @@ -0,0 +1,223 @@ +/* Copyright (C) 2018 the mpv developers + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "common/common.h" +#include "osdep/threads.h" +#include "osdep/timer.h" + +#include "thread_pool.h" + +// Threads destroy themselves after this many seconds, if there's no new work +// and the thread count is above the configured minimum. +#define DESTROY_TIMEOUT 10 + +struct work { + void (*fn)(void *ctx); + void *fn_ctx; +}; + +struct mp_thread_pool { + int min_threads, max_threads; + + mp_mutex lock; + mp_cond wakeup; + + // --- the following fields are protected by lock + + mp_thread *threads; + int num_threads; + + // Number of threads which have taken up work and are still processing it. + int busy_threads; + + bool terminate; + + struct work *work; + int num_work; +}; + +static MP_THREAD_VOID worker_thread(void *arg) +{ + struct mp_thread_pool *pool = arg; + + mp_thread_set_name("worker"); + + mp_mutex_lock(&pool->lock); + + int64_t destroy_deadline = 0; + bool got_timeout = false; + while (1) { + struct work work = {0}; + if (pool->num_work > 0) { + work = pool->work[pool->num_work - 1]; + pool->num_work -= 1; + } + + if (!work.fn) { + if (got_timeout || pool->terminate) + break; + + if (pool->num_threads > pool->min_threads) { + if (!destroy_deadline) + destroy_deadline = mp_time_ns() + MP_TIME_S_TO_NS(DESTROY_TIMEOUT); + if (mp_cond_timedwait_until(&pool->wakeup, &pool->lock, destroy_deadline)) + got_timeout = pool->num_threads > pool->min_threads; + } else { + mp_cond_wait(&pool->wakeup, &pool->lock); + } + continue; + } + + pool->busy_threads += 1; + mp_mutex_unlock(&pool->lock); + + work.fn(work.fn_ctx); + + mp_mutex_lock(&pool->lock); + pool->busy_threads -= 1; + + destroy_deadline = 0; + got_timeout = false; + } + + // If no termination signal was given, it must mean we died because of a + // timeout, and nobody is waiting for us. We have to remove ourselves. + if (!pool->terminate) { + for (int n = 0; n < pool->num_threads; n++) { + if (mp_thread_id_equal(mp_thread_get_id(pool->threads[n]), + mp_thread_current_id())) + { + mp_thread_detach(pool->threads[n]); + MP_TARRAY_REMOVE_AT(pool->threads, pool->num_threads, n); + mp_mutex_unlock(&pool->lock); + MP_THREAD_RETURN(); + } + } + MP_ASSERT_UNREACHABLE(); + } + + mp_mutex_unlock(&pool->lock); + MP_THREAD_RETURN(); +} + +static void thread_pool_dtor(void *ctx) +{ + struct mp_thread_pool *pool = ctx; + + + mp_mutex_lock(&pool->lock); + + pool->terminate = true; + mp_cond_broadcast(&pool->wakeup); + + mp_thread *threads = pool->threads; + int num_threads = pool->num_threads; + + pool->threads = NULL; + pool->num_threads = 0; + + mp_mutex_unlock(&pool->lock); + + for (int n = 0; n < num_threads; n++) + mp_thread_join(threads[n]); + + assert(pool->num_work == 0); + assert(pool->num_threads == 0); + mp_cond_destroy(&pool->wakeup); + mp_mutex_destroy(&pool->lock); +} + +static bool add_thread(struct mp_thread_pool *pool) +{ + mp_thread thread; + + if (mp_thread_create(&thread, worker_thread, pool) != 0) + return false; + + MP_TARRAY_APPEND(pool, pool->threads, pool->num_threads, thread); + return true; +} + +struct mp_thread_pool *mp_thread_pool_create(void *ta_parent, int init_threads, + int min_threads, int max_threads) +{ + assert(min_threads >= 0); + assert(init_threads <= min_threads); + assert(max_threads > 0 && max_threads >= min_threads); + + struct mp_thread_pool *pool = talloc_zero(ta_parent, struct mp_thread_pool); + talloc_set_destructor(pool, thread_pool_dtor); + + mp_mutex_init(&pool->lock); + mp_cond_init(&pool->wakeup); + + pool->min_threads = min_threads; + pool->max_threads = max_threads; + + mp_mutex_lock(&pool->lock); + for (int n = 0; n < init_threads; n++) + add_thread(pool); + bool ok = pool->num_threads >= init_threads; + mp_mutex_unlock(&pool->lock); + + if (!ok) + TA_FREEP(&pool); + + return pool; +} + +static bool thread_pool_add(struct mp_thread_pool *pool, void (*fn)(void *ctx), + void *fn_ctx, bool allow_queue) +{ + bool ok = true; + + assert(fn); + + mp_mutex_lock(&pool->lock); + struct work work = {fn, fn_ctx}; + + // If there are not enough threads to process all at once, but we can + // create a new thread, then do so. If work is queued quickly, it can + // happen that not all available threads have picked up work yet (up to + // num_threads - busy_threads threads), which has to be accounted for. + if (pool->busy_threads + pool->num_work + 1 > pool->num_threads && + pool->num_threads < pool->max_threads) + { + if (!add_thread(pool)) { + // If we can queue it, it'll get done as long as there is 1 thread. + ok = allow_queue && pool->num_threads > 0; + } + } + + if (ok) { + MP_TARRAY_INSERT_AT(pool, pool->work, pool->num_work, 0, work); + mp_cond_signal(&pool->wakeup); + } + + mp_mutex_unlock(&pool->lock); + return ok; +} + +bool mp_thread_pool_queue(struct mp_thread_pool *pool, void (*fn)(void *ctx), + void *fn_ctx) +{ + return thread_pool_add(pool, fn, fn_ctx, true); +} + +bool mp_thread_pool_run(struct mp_thread_pool *pool, void (*fn)(void *ctx), + void *fn_ctx) +{ + return thread_pool_add(pool, fn, fn_ctx, false); +} diff --git a/misc/thread_pool.h b/misc/thread_pool.h new file mode 100644 index 0000000..39106ee --- /dev/null +++ b/misc/thread_pool.h @@ -0,0 +1,35 @@ +#ifndef MPV_MP_THREAD_POOL_H +#define MPV_MP_THREAD_POOL_H + +#include <stdbool.h> +struct mp_thread_pool; + +// Create a thread pool with the given number of worker threads. This can return +// NULL if the worker threads could not be created. The thread pool can be +// destroyed with talloc_free(pool), or indirectly with talloc_free(ta_parent). +// If there are still work items on freeing, it will block until all work items +// are done, and the threads terminate. +// init_threads is the number of threads created in this function (and it fails +// if it could not be done). min_threads must be >=, if it's >, then the +// remaining threads will be created on demand, but never destroyed. +// If init_threads > 0, then mp_thread_pool_queue() can never fail. +// If init_threads == 0, mp_thread_pool_create() itself can never fail. +struct mp_thread_pool *mp_thread_pool_create(void *ta_parent, int init_threads, + int min_threads, int max_threads); + +// Queue a function to be run on a worker thread: fn(fn_ctx) +// If no worker thread is currently available, it's appended to a list in memory +// with unbounded size. This function always returns immediately. +// Concurrent queue calls are allowed, as long as it does not overlap with +// pool destruction. +// This function is explicitly thread-safe. +// Cannot fail if thread pool was created with at least 1 thread. +bool mp_thread_pool_queue(struct mp_thread_pool *pool, void (*fn)(void *ctx), + void *fn_ctx); + +// Like mp_thread_pool_queue(), but only queue the item and succeed if a thread +// can be reserved for the item (i.e. minimal wait time instead of unbounded). +bool mp_thread_pool_run(struct mp_thread_pool *pool, void (*fn)(void *ctx), + void *fn_ctx); + +#endif diff --git a/misc/thread_tools.c b/misc/thread_tools.c new file mode 100644 index 0000000..0f7fe8f --- /dev/null +++ b/misc/thread_tools.c @@ -0,0 +1,276 @@ +/* Copyright (C) 2018 the mpv developers + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <assert.h> +#include <errno.h> +#include <stdatomic.h> +#include <string.h> +#include <sys/types.h> +#include <unistd.h> + +#ifdef __MINGW32__ +#include <windows.h> +#else +#include <poll.h> +#endif + +#include "common/common.h" +#include "misc/linked_list.h" +#include "osdep/io.h" +#include "osdep/timer.h" + +#include "thread_tools.h" + +uintptr_t mp_waiter_wait(struct mp_waiter *waiter) +{ + mp_mutex_lock(&waiter->lock); + while (!waiter->done) + mp_cond_wait(&waiter->wakeup, &waiter->lock); + mp_mutex_unlock(&waiter->lock); + + uintptr_t ret = waiter->value; + + // We document that after mp_waiter_wait() the waiter object becomes + // invalid. (It strictly returns only after mp_waiter_wakeup() has returned, + // and the object is "single-shot".) So destroy it here. + + // Normally, we expect that the system uses futexes, in which case the + // following functions will do nearly nothing. This is true for Windows + // and Linux. But some lesser OSes still might allocate kernel objects + // when initializing mutexes, so destroy them here. + mp_mutex_destroy(&waiter->lock); + mp_cond_destroy(&waiter->wakeup); + + memset(waiter, 0xCA, sizeof(*waiter)); // for debugging + + return ret; +} + +void mp_waiter_wakeup(struct mp_waiter *waiter, uintptr_t value) +{ + mp_mutex_lock(&waiter->lock); + assert(!waiter->done); + waiter->done = true; + waiter->value = value; + mp_cond_signal(&waiter->wakeup); + mp_mutex_unlock(&waiter->lock); +} + +bool mp_waiter_poll(struct mp_waiter *waiter) +{ + mp_mutex_lock(&waiter->lock); + bool r = waiter->done; + mp_mutex_unlock(&waiter->lock); + return r; +} + +struct mp_cancel { + mp_mutex lock; + mp_cond wakeup; + + // Semaphore state and "mirrors". + atomic_bool triggered; + void (*cb)(void *ctx); + void *cb_ctx; + int wakeup_pipe[2]; + void *win32_event; // actually HANDLE + + // Slave list. These are automatically notified as well. + struct { + struct mp_cancel *head, *tail; + } slaves; + + // For slaves. Synchronization is managed by parent.lock! + struct mp_cancel *parent; + struct { + struct mp_cancel *next, *prev; + } siblings; +}; + +static void cancel_destroy(void *p) +{ + struct mp_cancel *c = p; + + assert(!c->slaves.head); // API user error + + mp_cancel_set_parent(c, NULL); + + if (c->wakeup_pipe[0] >= 0) { + close(c->wakeup_pipe[0]); + close(c->wakeup_pipe[1]); + } + +#ifdef __MINGW32__ + if (c->win32_event) + CloseHandle(c->win32_event); +#endif + + mp_mutex_destroy(&c->lock); + mp_cond_destroy(&c->wakeup); +} + +struct mp_cancel *mp_cancel_new(void *talloc_ctx) +{ + struct mp_cancel *c = talloc_ptrtype(talloc_ctx, c); + talloc_set_destructor(c, cancel_destroy); + *c = (struct mp_cancel){ + .triggered = false, + .wakeup_pipe = {-1, -1}, + }; + mp_mutex_init(&c->lock); + mp_cond_init(&c->wakeup); + return c; +} + +static void trigger_locked(struct mp_cancel *c) +{ + atomic_store(&c->triggered, true); + + mp_cond_broadcast(&c->wakeup); // condition bound to c->triggered + + if (c->cb) + c->cb(c->cb_ctx); + + for (struct mp_cancel *sub = c->slaves.head; sub; sub = sub->siblings.next) + mp_cancel_trigger(sub); + + if (c->wakeup_pipe[1] >= 0) + (void)write(c->wakeup_pipe[1], &(char){0}, 1); + +#ifdef __MINGW32__ + if (c->win32_event) + SetEvent(c->win32_event); +#endif +} + +void mp_cancel_trigger(struct mp_cancel *c) +{ + mp_mutex_lock(&c->lock); + trigger_locked(c); + mp_mutex_unlock(&c->lock); +} + +void mp_cancel_reset(struct mp_cancel *c) +{ + mp_mutex_lock(&c->lock); + + atomic_store(&c->triggered, false); + + if (c->wakeup_pipe[0] >= 0) { + // Flush it fully. + while (1) { + int r = read(c->wakeup_pipe[0], &(char[256]){0}, 256); + if (r <= 0 && !(r < 0 && errno == EINTR)) + break; + } + } + +#ifdef __MINGW32__ + if (c->win32_event) + ResetEvent(c->win32_event); +#endif + + mp_mutex_unlock(&c->lock); +} + +bool mp_cancel_test(struct mp_cancel *c) +{ + return c ? atomic_load_explicit(&c->triggered, memory_order_relaxed) : false; +} + +bool mp_cancel_wait(struct mp_cancel *c, double timeout) +{ + int64_t wait_until = mp_time_ns_add(mp_time_ns(), timeout); + mp_mutex_lock(&c->lock); + while (!mp_cancel_test(c)) { + if (mp_cond_timedwait_until(&c->wakeup, &c->lock, wait_until)) + break; + } + mp_mutex_unlock(&c->lock); + + return mp_cancel_test(c); +} + +// If a new notification mechanism was added, and the mp_cancel state was +// already triggered, make sure the newly added mechanism is also triggered. +static void retrigger_locked(struct mp_cancel *c) +{ + if (mp_cancel_test(c)) + trigger_locked(c); +} + +void mp_cancel_set_cb(struct mp_cancel *c, void (*cb)(void *ctx), void *ctx) +{ + mp_mutex_lock(&c->lock); + c->cb = cb; + c->cb_ctx = ctx; + retrigger_locked(c); + mp_mutex_unlock(&c->lock); +} + +void mp_cancel_set_parent(struct mp_cancel *slave, struct mp_cancel *parent) +{ + // We can access c->parent without synchronization, because: + // - concurrent mp_cancel_set_parent() calls to slave are not allowed + // - slave->parent needs to stay valid as long as the slave exists + if (slave->parent == parent) + return; + if (slave->parent) { + mp_mutex_lock(&slave->parent->lock); + LL_REMOVE(siblings, &slave->parent->slaves, slave); + mp_mutex_unlock(&slave->parent->lock); + } + slave->parent = parent; + if (slave->parent) { + mp_mutex_lock(&slave->parent->lock); + LL_APPEND(siblings, &slave->parent->slaves, slave); + retrigger_locked(slave->parent); + mp_mutex_unlock(&slave->parent->lock); + } +} + +int mp_cancel_get_fd(struct mp_cancel *c) +{ + mp_mutex_lock(&c->lock); + if (c->wakeup_pipe[0] < 0) { +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wstringop-overflow=" +#endif + mp_make_wakeup_pipe(c->wakeup_pipe); +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic pop +#endif + retrigger_locked(c); + } + mp_mutex_unlock(&c->lock); + + + return c->wakeup_pipe[0]; +} + +#ifdef __MINGW32__ +void *mp_cancel_get_event(struct mp_cancel *c) +{ + mp_mutex_lock(&c->lock); + if (!c->win32_event) { + c->win32_event = CreateEventW(NULL, TRUE, FALSE, NULL); + retrigger_locked(c); + } + mp_mutex_unlock(&c->lock); + + return c->win32_event; +} +#endif diff --git a/misc/thread_tools.h b/misc/thread_tools.h new file mode 100644 index 0000000..a07257b --- /dev/null +++ b/misc/thread_tools.h @@ -0,0 +1,83 @@ +#pragma once + +#include <stdint.h> +#include <stdbool.h> + +#include "osdep/threads.h" + +// This is basically a single-shot semaphore, intended as light-weight solution +// for just making a thread wait for another thread. +struct mp_waiter { + // All fields are considered private. Use MP_WAITER_INITIALIZER to init. + mp_mutex lock; + mp_cond wakeup; + bool done; + uintptr_t value; +}; + +// Initialize a mp_waiter object for use with mp_waiter_*(). +#define MP_WAITER_INITIALIZER { \ + .lock = MP_STATIC_MUTEX_INITIALIZER, \ + .wakeup = MP_STATIC_COND_INITIALIZER, \ + } + +// Block until some other thread calls mp_waiter_wakeup(). The function returns +// the value argument of that wakeup call. After this, the waiter object must +// not be used anymore. Although you can reinit it with MP_WAITER_INITIALIZER +// (then you must make sure nothing calls mp_waiter_wakeup() before this). +uintptr_t mp_waiter_wait(struct mp_waiter *waiter); + +// Unblock the thread waiting with mp_waiter_wait(), and make it return the +// provided value. If the other thread did not enter that call yet, it will +// return immediately once it does (mp_waiter_wakeup() always returns +// immediately). Calling this more than once is not allowed. +void mp_waiter_wakeup(struct mp_waiter *waiter, uintptr_t value); + +// Query whether the waiter was woken up. If true, mp_waiter_wait() will return +// immediately. This is useful if you want to use another way to block and +// wakeup (in parallel to mp_waiter). +// You still need to call mp_waiter_wait() to free resources. +bool mp_waiter_poll(struct mp_waiter *waiter); + +// Basically a binary semaphore that supports signaling the semaphore value to +// a bunch of other complicated mechanisms (such as wakeup pipes). It was made +// for aborting I/O and thus has according naming. +struct mp_cancel; + +struct mp_cancel *mp_cancel_new(void *talloc_ctx); + +// Request abort. +void mp_cancel_trigger(struct mp_cancel *c); + +// Return whether the caller should abort. +// For convenience, c==NULL is allowed. +bool mp_cancel_test(struct mp_cancel *c); + +// Wait until the even is signaled. If the timeout (in seconds) expires, return +// false. timeout==0 polls, timeout<0 waits forever. +bool mp_cancel_wait(struct mp_cancel *c, double timeout); + +// Restore original state. (Allows reusing a mp_cancel.) +void mp_cancel_reset(struct mp_cancel *c); + +// Add a callback to invoke when mp_cancel gets triggered. If it's already +// triggered, call it from mp_cancel_add_cb() directly. May be called multiple +// times even if the trigger state changes; not called if it resets. In all +// cases, this may be called with internal locks held (either in mp_cancel, or +// other locks held by whoever calls mp_cancel_trigger()). +// There is only one callback. Create a slave mp_cancel to get a private one. +void mp_cancel_set_cb(struct mp_cancel *c, void (*cb)(void *ctx), void *ctx); + +// If parent gets triggered, automatically trigger slave. There is only 1 +// parent; setting NULL clears the parent. Freeing slave also automatically +// ends the parent link, but the parent mp_cancel must remain valid until the +// slave is manually removed or destroyed. Destroying a mp_cancel that still +// has slaves is an error. +void mp_cancel_set_parent(struct mp_cancel *slave, struct mp_cancel *parent); + +// win32 "Event" HANDLE that indicates the current mp_cancel state. +void *mp_cancel_get_event(struct mp_cancel *c); + +// The FD becomes readable if mp_cancel_test() would return true. +// Don't actually read from it, just use it for poll(). +int mp_cancel_get_fd(struct mp_cancel *c); diff --git a/misc/uuid.c b/misc/uuid.c new file mode 100644 index 0000000..c739b3c --- /dev/null +++ b/misc/uuid.c @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2022 Pierre-Anthony Lemieux <pal@palemieux.com> + * Zane van Iperen <zane@zanevaniperen.com> + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* + * Copyright (C) 1996, 1997 Theodore Ts'o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, and the entire permission notice in its entirety, + * including the disclaimer of warranties. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ALL OF + * WHICH ARE HEREBY DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF NOT ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + */ + +/** + * @file + * UUID parsing and serialization utilities. + * The library treat the UUID as an opaque sequence of 16 unsigned bytes, + * i.e. ignoring the internal layout of the UUID, which depends on the type + * of the UUID. + * + * @author Pierre-Anthony Lemieux <pal@palemieux.com> + * @author Zane van Iperen <zane@zanevaniperen.com> + */ + +#include "uuid.h" +#include "libavutil/error.h" +#include "libavutil/avstring.h" + +int av_uuid_parse(const char *in, AVUUID uu) +{ + if (strlen(in) != 36) + return AVERROR(EINVAL); + + return av_uuid_parse_range(in, in + 36, uu); +} + +static int xdigit_to_int(char c) +{ + c = av_tolower(c); + + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + if (c >= '0' && c <= '9') + return c - '0'; + + return -1; +} + +int av_uuid_parse_range(const char *in_start, const char *in_end, AVUUID uu) +{ + int i; + const char *cp; + + if ((in_end - in_start) != 36) + return AVERROR(EINVAL); + + for (i = 0, cp = in_start; i < 16; i++) { + int hi; + int lo; + + if (i == 4 || i == 6 || i == 8 || i == 10) + cp++; + + hi = xdigit_to_int(*cp++); + lo = xdigit_to_int(*cp++); + + if (hi == -1 || lo == -1) + return AVERROR(EINVAL); + + uu[i] = (hi << 4) + lo; + } + + return 0; +} + +static const char hexdigits_lower[16] = "0123456789abcdef"; + +void av_uuid_unparse(const AVUUID uuid, char *out) +{ + char *p = out; + + for (int i = 0; i < 16; i++) { + uint8_t tmp; + + if (i == 4 || i == 6 || i == 8 || i == 10) + *p++ = '-'; + + tmp = uuid[i]; + *p++ = hexdigits_lower[tmp >> 4]; + *p++ = hexdigits_lower[tmp & 15]; + } + + *p = '\0'; +} + +int av_uuid_urn_parse(const char *in, AVUUID uu) +{ + if (av_stristr(in, "urn:uuid:") != in) + return AVERROR(EINVAL); + + return av_uuid_parse(in + 9, uu); +} diff --git a/misc/uuid.h b/misc/uuid.h new file mode 100644 index 0000000..748b7ed --- /dev/null +++ b/misc/uuid.h @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2022 Pierre-Anthony Lemieux <pal@palemieux.com> + * Zane van Iperen <zane@zanevaniperen.com> + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * UUID parsing and serialization utilities. + * The library treats the UUID as an opaque sequence of 16 unsigned bytes, + * i.e. ignoring the internal layout of the UUID, which depends on the type + * of the UUID. + * + * @author Pierre-Anthony Lemieux <pal@palemieux.com> + * @author Zane van Iperen <zane@zanevaniperen.com> + */ + +#ifndef AVUTIL_UUID_H +#define AVUTIL_UUID_H + +#include <stdint.h> +#include <string.h> + +#define AV_PRI_UUID \ + "%02hhx%02hhx%02hhx%02hhx-%02hhx%02hhx-" \ + "%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx" + +#define AV_PRI_URN_UUID \ + "urn:uuid:%02hhx%02hhx%02hhx%02hhx-%02hhx%02hhx-" \ + "%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx" + +/* AV_UUID_ARG() is used together with AV_PRI_UUID() or AV_PRI_URN_UUID + * to print UUIDs, e.g. + * av_log(NULL, AV_LOG_DEBUG, "UUID: " AV_PRI_UUID, AV_UUID_ARG(uuid)); + */ +#define AV_UUID_ARG(x) \ + (x)[ 0], (x)[ 1], (x)[ 2], (x)[ 3], \ + (x)[ 4], (x)[ 5], (x)[ 6], (x)[ 7], \ + (x)[ 8], (x)[ 9], (x)[10], (x)[11], \ + (x)[12], (x)[13], (x)[14], (x)[15] + +#define AV_UUID_LEN 16 + +/* Binary representation of a UUID */ +typedef uint8_t AVUUID[AV_UUID_LEN]; + +/** + * Parses a string representation of a UUID formatted according to IETF RFC 4122 + * into an AVUUID. The parsing is case-insensitive. The string must be 37 + * characters long, including the terminating NUL character. + * + * Example string representation: "2fceebd0-7017-433d-bafb-d073a7116696" + * + * @param[in] in String representation of a UUID, + * e.g. 2fceebd0-7017-433d-bafb-d073a7116696 + * @param[out] uu AVUUID + * @return A non-zero value in case of an error. + */ +int av_uuid_parse(const char *in, AVUUID uu); + +/** + * Parses a URN representation of a UUID, as specified at IETF RFC 4122, + * into an AVUUID. The parsing is case-insensitive. The string must be 46 + * characters long, including the terminating NUL character. + * + * Example string representation: "urn:uuid:2fceebd0-7017-433d-bafb-d073a7116696" + * + * @param[in] in URN UUID + * @param[out] uu AVUUID + * @return A non-zero value in case of an error. + */ +int av_uuid_urn_parse(const char *in, AVUUID uu); + +/** + * Parses a string representation of a UUID formatted according to IETF RFC 4122 + * into an AVUUID. The parsing is case-insensitive. + * + * @param[in] in_start Pointer to the first character of the string representation + * @param[in] in_end Pointer to the character after the last character of the + * string representation. That memory location is never + * accessed. It is an error if `in_end - in_start != 36`. + * @param[out] uu AVUUID + * @return A non-zero value in case of an error. + */ +int av_uuid_parse_range(const char *in_start, const char *in_end, AVUUID uu); + +/** + * Serializes a AVUUID into a string representation according to IETF RFC 4122. + * The string is lowercase and always 37 characters long, including the + * terminating NUL character. + * + * @param[in] uu AVUUID + * @param[out] out Pointer to an array of no less than 37 characters. + */ +void av_uuid_unparse(const AVUUID uu, char *out); + +/** + * Compares two UUIDs for equality. + * + * @param[in] uu1 AVUUID + * @param[in] uu2 AVUUID + * @return Nonzero if uu1 and uu2 are identical, 0 otherwise + */ +static inline int av_uuid_equal(const AVUUID uu1, const AVUUID uu2) +{ + return memcmp(uu1, uu2, AV_UUID_LEN) == 0; +} + +/** + * Copies the bytes of src into dest. + * + * @param[out] dest AVUUID + * @param[in] src AVUUID + */ +static inline void av_uuid_copy(AVUUID dest, const AVUUID src) +{ + memcpy(dest, src, AV_UUID_LEN); +} + +/** + * Sets a UUID to the nil UUID, i.e. a UUID with have all + * its 128 bits set to zero. + * + * @param[in,out] uu UUID to be set to the nil UUID + */ +static inline void av_uuid_nil(AVUUID uu) +{ + memset(uu, 0, AV_UUID_LEN); +} + +#endif /* AVUTIL_UUID_H */ |