SDL/Windows 代码分析【1】thread

1. SDL_semaphore

代码:src\thread\windows\SDL_syssem.c

别名:SDL_sem

1
typedef struct SDL_semaphore SDL_sem;

基于 WinAPI 匿名 Semaphore 封装。MaximumCount 硬编码为 32 * 1024。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct SDL_semaphore
{
HANDLE id;
LONG count;
};


/* Create a semaphore */
SDL_sem *
SDL_CreateSemaphore(Uint32 initial_value)
{
SDL_sem *sem;

/* Allocate sem memory */
sem = (SDL_sem *) SDL_malloc(sizeof(*sem));
if (sem) {
/* Create the semaphore, with max value 32K */
#if __WINRT__
sem->id = CreateSemaphoreEx(NULL, initial_value, 32 * 1024, NULL, 0, SEMAPHORE_ALL_ACCESS);
#else
sem->id = CreateSemaphore(NULL, initial_value, 32 * 1024, NULL);
#endif
sem->count = initial_value;
if (!sem->id) {
SDL_SetError("Couldn't create semaphore");
SDL_free(sem);
sem = NULL;
}
} else {
SDL_OutOfMemory();
}
return (sem);
}

2. SDL_mutex

代码:src\thread\windows\SDL_sysmutex.c

基于 WinAPI CriticalSection 封装。SpinCount 硬编码为 2000,即在多处理器系统上,如果无法立刻进入临界区,则会自旋最多 2000 次,然后等待 CriticalSection 内部关联的信号量。只要在自旋过程中其它线程退出临界区,则无需进入等待状态。这么做是提高效率,自旋时当前线程还占着 CPU,如果进入等待状态,就是交出 CPU 时间片了,而 CPU 调度是个消耗型操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct SDL_mutex
{
CRITICAL_SECTION cs;
};

/* Create a mutex */
SDL_mutex *
SDL_CreateMutex(void)
{
SDL_mutex *mutex;

/* Allocate mutex memory */
mutex = (SDL_mutex *) SDL_malloc(sizeof(*mutex));
if (mutex) {
/* Initialize */
/* On SMP systems, a non-zero spin count generally helps performance */
#if __WINRT__
InitializeCriticalSectionEx(&mutex->cs, 2000, 0);
#else
InitializeCriticalSectionAndSpinCount(&mutex->cs, 2000);
#endif
} else {
SDL_OutOfMemory();
}
return (mutex);
}

3. SDL_cond

代码:src\thread\windows\SDL_syscond.c

基于 SDL_mutex 和 SDL_sem 封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct SDL_cond
{
SDL_mutex *lock;
int waiting;
int signals;
SDL_sem *wait_sem;
SDL_sem *wait_done;
};

/* Create a condition variable */
SDL_cond *
SDL_CreateCond(void)
{
SDL_cond *cond;

cond = (SDL_cond *) SDL_malloc(sizeof(SDL_cond));
if (cond) {
cond->lock = SDL_CreateMutex();
cond->wait_sem = SDL_CreateSemaphore(0);
cond->wait_done = SDL_CreateSemaphore(0);
cond->waiting = cond->signals = 0;
if (!cond->lock || !cond->wait_sem || !cond->wait_done) {
SDL_DestroyCond(cond);
cond = NULL;
}
} else {
SDL_OutOfMemory();
}
return (cond);
}

SDL_CondWaitTimeout 实现较长,本文忽略。重点是:为了避免死锁,它进入等待前,会先解锁第二个参数 mutex。如果不这么做,其它线程也要 Lock 这个 mutex 就会发生死锁。

以下代码是典型用法,线程 A 进入临界区后,SDL_CondWait(内部调用 SDL_CondWaitTimeout)会调用 SDL_UnlockMutex(lock); 使得线程 B 可以进入临界区调用 SDL_CondSignal(cond);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Typical use

// Thread A:
SDL_LockMutex(lock);
while (!condition) {
SDL_CondWait(cond, lock);
}
SDL_UnlockMutex(lock);

// Thread B:
SDL_LockMutex(lock);
condition = true;
SDL_CondSignal(cond);
SDL_UnlockMutex(lock);

4. SDL_Atomic

代码:src\atomic\SDL_atomic.c

基于 _Interlocked API 封装。此类原子操作一般底层实现都是相应平台的汇编指令(比如 x86 平台是 lock cmpxchg 之类),但在不同平台下会有不同的封装集,所以 SDL_atomic.c 里有很多平台相关的宏判断。

5. SDL_MemoryBarrier

代码:src\atomic\SDL_atomic.h

内存屏障。参考文章:Acquire and Release Semantics

在 Windows x86 环境下等价于 _ReadWriteBarrier:

1
2
3
4
5
6
7
void _ReadWriteBarrier(void);
#pragma intrinsic(_ReadWriteBarrier)
#define SDL_CompilerBarrier() _ReadWriteBarrier()

/* This is correct for the x86 and x64 CPUs, and we'll expand this over time. */
#define SDL_MemoryBarrierRelease() SDL_CompilerBarrier()
#define SDL_MemoryBarrierAcquire() SDL_CompilerBarrier()
  • Acquire semantics is a property that can only apply to operations that read from shared memory, whether they are read-modify-write operations or plain loads. The operation is then considered a read-acquire. Acquire semantics prevent memory reordering of the read-acquire with any read or write operation that follows it in program order.

  • Release semantics is a property that can only apply to operations that write to shared memory, whether they are read-modify-write operations or plain stores. The operation is then considered a write-release. Release semantics prevent memory reordering of the write-release with any read or write operation that precedes it in program order.

生硬的翻译如下:

  • 获取语义是一个属性,它只能应用于从共享内存读取的操作,无论是“读取-修改-写入”操作还是普通加载。该操作将被视为“读取获取”。获取语义可防止“读取获取”和它之后的读取或写入操作发生内存重新排序。

  • 释放语义是一个属性,它只能应用于写入共享内存的操作,无论是“读取-修改-写入”操作还是普通存储。该操作将被视为“写入释放”。释放语义可防止“写入释放”和它之前,它之前的任何读取或写入操作发生内存重新排序。

以 x86 内存模型为例说明:

  • Loads are not reordered with other loads.
  • Stores are not reordered with other stores.
  • Stores are not reordered with older loads.
  • Loads may be reordered with older stores to different locations.

因为 store-load 可以被重排,所以 x86 不是顺序一致。但是其他三种读写顺序不能被重排,所以 x86 是 acquire/release 语义。

aquire 语义:load 之后的读写操作无法被重排至 load 之前。即 load-load, load-store 不能被重排。

release 语义:store 之前的读写操作无法被重排至 store 之后。即 load-store, store-store 不能被重排。

6. SDL_TLSData

意义:TLS,即 Thread Local Storage(线程局部存储)。

代码:src\thread\SDL_thread_c.h 和 src\thread\windows\SDL_systls.c

基于 SDL_Atomic、SDL_MemoryBarrier 和 WinAPI Tls API 封装。

以下结构体包含一个析构函数的指针,非空时,SDL_TLSCleanup() 会调用它。

1
2
3
4
5
6
7
8
/* This is the system-independent thread local storage structure */
typedef struct {
unsigned int limit;
struct {
void *data;
void (SDLCALL *destructor)(void*);
} array[1];
} SDL_TLSData;

7. SDL_Thread

代码:src\thread\SDL_thread.c 和 src\thread\windows\SDL_systhread.c

创建线程的 API 是 SDL_CreateThread 和 SDL_CreateThreadWithStackSize,导出函数 SDL_CreateThread 的定义如下,记为【X】:

1
SDL_DYNAPI_PROC(SDL_Thread*,SDL_CreateThread,(SDL_ThreadFunction a, const char *b, void *c, pfnSDL_CurrentBeginThread d, pfnSDL_CurrentEndThread e),(a,b,c,d,e),return)

下面会有递归展开宏的过程。首先,用 SDL_DYNAPI_PROC 的定义:

1
2
3
4
5
#define SDL_DYNAPI_PROC(rc,fn,params,args,ret) \
static rc SDLCALL fn##_DEFAULT params { \
SDL_InitDynamicAPI(); \
ret jump_table.fn args; \
}

展开【X】得到:

1
2
3
4
static SDL_Thread* __cdecl SDL_CreateThread(SDL_ThreadFunction a, const char *b, void *c, pfnSDL_CurrentBeginThread d, pfnSDL_CurrentEndThread e) {
SDL_InitDynamicAPI();
return jump_table.SDL_CreateThread(a,b,c,d,e);
}

其中 jump_table.SDL_CreateThread 是【X】被 SDL_dynapi_procs.h 的:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* The jump table! */
typedef struct {
#define SDL_DYNAPI_PROC(rc,fn,params,args,ret) SDL_DYNAPIFN_##fn fn;
#include "SDL_dynapi_procs.h"
#undef SDL_DYNAPI_PROC
} SDL_DYNAPI_jump_table;

/* The actual jump table. */
static SDL_DYNAPI_jump_table jump_table = {
#define SDL_DYNAPI_PROC(rc,fn,params,args,ret) fn##_DEFAULT,
#include "SDL_dynapi_procs.h"
#undef SDL_DYNAPI_PROC
};

展开得到,为:

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
/* ... */
SDL_DYNAPIFN_SDL_CreateThread SDL_CreateThread;
/* ... */
} SDL_DYNAPI_jump_table;

static SDL_DYNAPI_jump_table jump_table = {
/* ... */
SDL_CreateThread_DEFAULT,
/* ... */
};

【X】又被 SDL_dynapi.c 的 initialize_jumptable 函数的:

1
2
3
4
/* Init our jump table first. */
#define SDL_DYNAPI_PROC(rc,fn,params,args,ret) jump_table.fn = fn##_REAL;
#include "SDL_dynapi_procs.h"
#undef SDL_DYNAPI_PROC

展开为:

1
2
3
/* ... */
jump_table.SDL_CreateThread = SDL_CreateThread_REAL;
/* ... */

所以,调用 SDL_CreateThread 最终调用的就是 SDL_CreateThread_REAL,又由于 src\dynapi\SDL_dynapi_overrides.h 中的:

1
#define SDL_CreateThread SDL_CreateThread_REAL

所以调用的是 src\thread\SDL_thread.c 中的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#ifdef SDL_PASSED_BEGINTHREAD_ENDTHREAD
DECLSPEC SDL_Thread *SDLCALL
SDL_CreateThread(int (SDLCALL * fn) (void *),
const char *name, void *data,
pfnSDL_CurrentBeginThread pfnBeginThread,
pfnSDL_CurrentEndThread pfnEndThread)
#else
DECLSPEC SDL_Thread *SDLCALL
SDL_CreateThread(int (SDLCALL * fn) (void *),
const char *name, void *data)
#endif
{
/* !!! FIXME: in 2.1, just make stackhint part of the usual API. */
const char *stackhint = SDL_GetHint(SDL_HINT_THREAD_STACK_SIZE);
size_t stacksize = 0;

/* If the SDL_HINT_THREAD_STACK_SIZE exists, use it */
if (stackhint != NULL) {
char *endp = NULL;
const Sint64 hintval = SDL_strtoll(stackhint, &endp, 10);
if ((*stackhint != '\0') && (*endp == '\0')) { /* a valid number? */
if (hintval > 0) { /* reject bogus values. */
stacksize = (size_t) hintval;
}
}
}

#ifdef SDL_PASSED_BEGINTHREAD_ENDTHREAD
return SDL_CreateThreadWithStackSize(fn, name, stacksize, data, pfnBeginThread, pfnEndThread);
#else
return SDL_CreateThreadWithStackSize(fn, name, stacksize, data);
#endif
}

可见 SDL_CreateThread 调用了 SDL_CreateThreadWithStackSize,而 SDL_CreateThreadWithStackSize 又调用 src\thread\windows\SDL_systhread.c 中的 SDL_SYS_CreateThread,因为 Windows 平台有 _beginthreadex_endthreadex,所以最后是调用 _beginthreadex

1
2
3
4
5
6
7
8
9
10
11
12
13
/* thread->stacksize == 0 means "system default", same as win32 expects */
if (pfnBeginThread) {
unsigned threadid = 0;
thread->handle = (SYS_ThreadHandle)
((size_t) pfnBeginThread(NULL, (unsigned int) thread->stacksize,
RunThreadViaBeginThreadEx,
pThreadParms, flags, &threadid));
} else {
DWORD threadid = 0;
thread->handle = CreateThread(NULL, thread->stacksize,
RunThreadViaCreateThread,
pThreadParms, flags, &threadid);
}

其中 RunThreadViaBeginThreadEx 实际上是调用 RunThread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static DWORD
RunThread(void *data)
{
pThreadStartParms pThreadParms = (pThreadStartParms) data;
pfnSDL_CurrentEndThread pfnEndThread = pThreadParms->pfnCurrentEndThread;
void *args = pThreadParms->args;
SDL_free(pThreadParms);
SDL_RunThread(args);
if (pfnEndThread != NULL)
pfnEndThread(0);
return (0);
}

static DWORD WINAPI
RunThreadViaCreateThread(LPVOID data)
{
return RunThread(data);
}

static unsigned __stdcall
RunThreadViaBeginThreadEx(void *data)
{
return (unsigned) RunThread(data);
}

从代码可见 RunThread 调用 SDL_RunThread,而 SDL_RunThread 内部由 SDL_TLSCleanup() 来调用析构函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void
SDL_RunThread(void *data)
{
thread_args *args = (thread_args *) data;
int (SDLCALL * userfunc) (void *) = args->func;
void *userdata = args->data;
SDL_Thread *thread = args->info;
int *statusloc = &thread->status;

/* Perform any system-dependent setup - this function may not fail */
SDL_SYS_SetupThread(thread->name);

/* Get the thread id */
thread->threadid = SDL_ThreadID();

/* Wake up the parent thread */
SDL_SemPost(args->wait);

/* Run the function */
*statusloc = userfunc(userdata);

/* Clean up thread-local storage */
SDL_TLSCleanup();

/* Mark us as ready to be joined (or detached) */
if (!SDL_AtomicCAS(&thread->state, SDL_THREAD_STATE_ALIVE, SDL_THREAD_STATE_ZOMBIE)) {
/* Clean up if something already detached us. */
if (SDL_AtomicCAS(&thread->state, SDL_THREAD_STATE_DETACHED, SDL_THREAD_STATE_CLEANED)) {
if (thread->name) {
SDL_free(thread->name);
}
SDL_free(thread);
}
}
}

八哥之神前传【13】

618 年,识界

圣仙山:两位娘子,待会儿去吾姐梦里和她告别。

李心觎:为啥不在视界亲自和她告别呢?

圣仙山:她远嫁黑龙江,吾在视界又不能飞,要跑很远纳!再说吾在视界还没娶妻,多没面子呀。

李心觎:嘻嘻嘻。

李冰月:哈哈哈!姐姐,咱们是去给相公撑场面的!

李神源梦中

圣仙山:神姐!

李神源:嗯?是仙弟呀。这两位是?

圣仙山:是小弟的妻子。

李冰月、李心觎:姐姐好!

李神源:好好好。两位弟妹灵秀如仙,仙弟什么时候得此艳福,真是好大的惊喜呀!

李冰月:好说好说。相公才是灵秀如仙,我们姐妹俩还不如他呢!

李心觎:是呀,神姐,夫君才是真仙。

李神源:哎呀,我这仙弟以前老和我说,他要一心求道,不想娶妻生子,还说未来的人越来越多都不结婚的。

圣仙山:姐!现在情况有变,当今皇帝受老钧托梦,要老爹组织一群谷阳人去南荒开疆传道,同行人中有技师文人,也有流民和被大赦的罪犯,还要途径苦境各地,和三教九流打交道,我是最佳带队人选。

李神源:此去危险重重,仙弟为何不让别人去呢?

圣仙山:除了传道,吾还有别的任务,要去寻找吾心中的道州德国,一处远离天灾人祸的修道圣地。

李冰月:神姐放心,相公已经修成八识神通,这事可以安全办成。

李神源:既然如此,为姐只能祝你一切顺利。我养了一只守护灵狐,让它陪你上路吧,风餐露宿有危险时,它会警示你。

圣仙山:谢谢神姐。

识界

李心觎:神姐梦里的灵狐好可爱耶。

李冰月:是呀,咱们在识界里找一只呗!

李心觎:走。

李冰月:相公,这只叫“纯狐连玉”,你在视界如果遇到它,它会认得你的。

李心觎:连玉,暂时就陪我们吧。

少曾读仙史,知有苏耽君。流望来南国,依然会昔闻。
泊舟问耆老,遥指孤山云。孤山郴郡北,不与众山群。
重崖下萦映,嶛峣上纠纷。碧峰泉附落,红壁树傍分。
选地今方尔,升天因可云。不才予窜迹,羽化子遗芬。
将览成麟凤,旋惊御鬼文。此中迷出处,含思独氛氲。

2003 年

陈因提:圣仙山带着一群中原人来到现在你家这个地方?

圣小开:对!仙山公在路上娶了胡小玉,才有现在的我。

陈因提:胡小玉很特别吗?娶别人不一样有你?

圣小开:万一不娶呢?那我不是生不出来!

陈因提:哈哈。你真是瞎操心。我比较担心他死在路上,而你担心他不娶!

圣小开:有神通在,明显死不了呀。料事如神,有啥危险或者人际关系的问题,都能预防。

陈因提:哦,后世人神化的吧!成王败寇。

圣小开:仙山公能找到这个好地方本身就很神了,台风地震海啸全被周围其他地方挡住,几乎没天灾,还远离政治和战乱,在当时可谓世外桃源。

陈因提:这是现实,暂且信你。那么,你有遗传到这八识神通吗?

圣小开:略懂略懂!八识神通分为被动接受和主动测算,我有时候也能接受到来自未来的预警,主动测算我是不会。而且主动测算会耗费心力,即所谓招天谴。仙山公测算多次,导致 35 岁就仙逝了。胡小玉就活了 42 岁,他们女儿活到 64 岁。

陈因提:算出啥了?老天爷这么急着收他?

圣小开:回归十界。

陈因提:啥意思?

圣小开:不是老天收他,是他终于算出怎么回归十界,便抛下肉身……

陈因提:等等,说人话!

圣小开:算出怎么去死!

陈因提:切!还以为啥高深的法门……睡了睡了。

挨踢人必备 Embody

故事从 2012 年开始

这一年在 59 号大网吧办公,总感觉桌子太矮椅子太烂……心神不宁,频繁想起一个故事:

某同事很低调,从来不炫耀自己的成就,突然有一天,他说:“我太突出了!”

一问,原来是腰间盘突出……

这一年,还是哥的稣决定攒钱买一把好的椅子。

存了几个月,终于买了一把保友金豪 E-AB-HAM。那时好多同事来围观,有 HR 来说了句:“比总经理用的还好!”,也有不少小伙伴说长得像医疗用的复健椅。

**但是这把椅子在稣心里并没有地位。**当时已经有智能手机的稣,从来没有拍过它,以致如今想查具体啥时候买的,都不那么确定。只记得是花 3800 元买的,最晚不会超过 2013-01-19 这个时间!

大约的购买时间

当时还很撒逼地把能选配的全给配上……实际使用证实左手边的书架几乎用不上,以致后来把它给拆下来了!脚垫也不常用,很少在公司午睡。

说起效果,那确实比公司的乐射椅子好很多,但也不是特别舒服。网布很透气,但在公司有空调的前提下,透气并不是那么必须。反而缺点就是它并没有根据屁股的造型来支托,简单地说有点勒腿,坐久了并不舒服。所以,后来又研究起 Embody。

开始了解 Embody

后来有土豪要买 Embody,稣跟着去体验了一下。2020 年,土豪又买了第二张……

直到 2020 年才有钱买

2020-06-10 下单乐歌升降桌 E3,180*80cm。靠自动提醒功能养成坐久了站会儿的习惯,结果反而让稣发现八哥就在金豪这把椅子上!网布还是个问题,坐 8 年已经变软一些,但依然有一种压迫大腿两侧的酸麻感。

吓得稣赶紧下单,买了介个:

Embody 廉价替代品

这下终于明白形状和支撑的重要性!果断存钱买 Embody!

存了一个月多的钱,再加上脑波给的一万块人民币,终于入手 Embody!

体会

  • Embody 可以调得比金豪更矮,适合的身高范围就更大,尤其对女性友好。腿、屁股和腰都舒服。

  • 一张 Embody 价格可以买金豪 3 张以上,但是肯定是没有 3 倍的舒服,高端的东西性价比自然比大众款低的多,使用价值只能在细节上轻微地增加。好在大腿的问题真的解决了。

  • 挨踢族一天坐在椅子上的时间是远超健康上限的。稣并不赞成通过买任何东西来装逼,除非这东西是真实用,所以贵。Embody 对很多人来说,可能就是装逼,但对挨踢人绝对不是。

并不是主角才有光环,而是有光环的才能当主角,这才是生活。——泥巴娃

  • 挨踢人就应该对自己好一点。

最后,一句话总结:要不是穷,真想再买一张!

八哥之神前传【12】

识界

李仙山,字太墟,号道识,是最后一名通过修行进入识界的人类,最为人津津乐道的是他娶了两位貌美如花的妻子:李冰月和李心觎。三人同在深山修炼,羡煞旁人。

李仙山:两位娘子,山顶那朵魔莲原来是老钧的智慧所化,吾已得其认可,纳入脑识。

李冰月:那相公还是相公吗?

李心觎:是呀,夫君不会鲸神魂裂吗?

李仙山:哈哈,不仅没有副作用,还助吾练就八识神通。

李心觎:听起来好厉害哦!有何用处呢?

李冰月:哎呀!相公的眼睛怎么流血了!

李仙山:咦?难道不能预测自己的未来?

李心觎:怎么回事也?赶紧擦擦。

李仙山:八识神通能够预知未来,吾刚刚测算视界中自己的未来,发现未来的视界大部分地方都实行一夫一妻制,吾之转世因此无法娶三个妻子。

李冰月:测算自己会破坏因果循环,必遭天谴,相公可别乱用。

李仙山:只要不测自己就没事。吾试过预测识界未来,发现识界将关闭千余年,后有邪灵借助科技的力量强行进入识界,后使视界的普通人不必修行亦可进入识界。可谓千年浩劫!

李冰月:此事非同小可,需召集众仙请示老钧。

李仙山:不必如此兴师动众,为夫一人已可召唤老钧。

李心觎:老钧居于十维之外,三千年来只见众人祈祷之法,却无人见过老钧现身。夫君真能召唤出来?

李仙山:老钧的意志无处不在,只要用八识神通与他纠缠就可以接受他的意识传播。来!

突然云雾缭绕的道州蓬山化入星辰,创世天尊老钧自虚无中发出一道引力波。

李仙山:小孙,拜见老祖,请老钧指示应对浩劫之方。

老钧:道之不传,识界将倾。回到视界,去南方寻找道州德国。

李仙山:领道旨。

老钧:天机不可泄露!切记不可再测算自己的未来。从今而后,改姓圣吧!

李仙山:这个姓小孙恐怕承受不起呀!再说不久前才从老改为李的,又改?

老钧:视界中的李伯阳会助你,让当朝皇帝赐姓。

李仙山:遵旨!

老钧消失前,喃喃道:那厮岂非汝乎?小孙杂……

回到道州蓬山。

圣仙山:两位娘子,为传道法,吾将离开中原,奔波于南荒之地,恐少有时间再入识界。

李冰月:相公是去干大事的,不用担心我们,我们会照顾好自己的。

李心觎:对呀,夫君偶尔来看我们就好。小别胜新婚,也许更好呢!

圣仙山:哈哈哈!

李冰月:姐姐,我们写几句诗送给相公吧!

李心觎:好呀。妹妹是大老婆,你先来。

李冰月:晓镜但愁云鬓改,夜吟应觉月光寒。蓬山此去无多路,青鸟殷勤为探看。

李心觎:昨夜星辰昨夜风,画楼西畔桂堂东。身无彩凤双飞翼,心有灵犀一点通。

圣仙山:好诗!好诗!都是你们未来的荥阳老乡写的!

李冰月、李心觎:哈哈哈。

圣仙山:再过两百多年义山就出生了!哈哈哈……

2003 年

陈因提:圣仙山就是你老祖宗?

圣小开:当然,所以我遗传了他的特异功能。

陈因提:哦……我有点困!你最好拿出能提神的证据。

圣小开:我偶尔也能进入识界!一般人我不告诉他。

陈因提:好啦好啦,那你说说看,你都看到啥?

圣小开:众生皆苦!

陈因提:阿弥陀佛。我裤子都脱了,你给我念经?

圣小开:耶!你怎么知道我还写过一本经书。

陈因提:拿出来,我帮你烧给你老祖宗。

圣小开:切!这本《八哥之神创世手稿》十分珍贵,里面记载着进入识界的修炼法门。

陈因提:一本九毛九?我买,明天给你一百块,免找。

圣小开:给你也好,要是我突然遭天谴,起码这绝学不会就此失传。拿去,好好保管。

陈因提:哟,字体工整,比写情书还认真!那我就收了,放心吧,我当宝一样收藏。

牛媒体装逼录

听说鲁豫要来采访稣

3Q

鹿邑:听说稣已经从区块链开发转型为牛媒体开发,能说说牛媒体吗?

稣:牛媒体就是三个 Q。

鹿邑:您是说三个季度就能掌握牛媒体?

稣:不是……是说牛媒体不过就是三个队列。

鹿邑:哦?我不会问您是哪三个!

稣:第一个是网络包队列,第二个是音视频包队列,第三个是音视频帧队列。

转码

鹿邑:那经常听到的“转码”到底是啥?

稣:标准多到蛋疼罢了。你的化妆桌,东西怎么摆你说了算,别人摆法不同,但东西是一样的。你搬家时,把它们装起来,用啥来装,怎么装,扔掉哪些不重要的,也是你说了算,别人装法不同,但关键的几样东西还是一样的。

鹿邑:据说转码很烧钱,你们用的机器都很贵?

稣:没有没有,小米游戏本就够装逼。

光栅化

鹿邑:第一次听到“光栅化”这个词时,表示一脸懵逼,给解释一下呗?

稣:稣拿稣法做类比,您就能理解了!Look!

地头力,图片来自百度,如有侵权请联系本人删除。

鹿邑:这就是光栅化?还是没明白呀!

稣:别急!您看这些笔划……稣大笔一挥,横竖撇捺,点折弯钩,一笔走红尘,两笔惊鬼神。稣的内心只有笔法走势。

鹿邑:牛逼死了!赶紧发朋友圈!

稣:没错!关键就是发票圈!为了用现代化的方式传播稣法,咱们可以扫描打印、刊登到报纸、拍照发票圈,这些手段有个共同点——最后出来都是一些像素点。

鹿邑:还真是这样哦!我以前拿放大镜看过屏幕,都是红绿蓝的像素点。

稣:您的理解能力不错。这个把“笔划”变成一系列点的过程就是“光栅化”。举个例子,您在画板上拉一条直线,计算机只需要知道头尾两个点和颜色就能定义这条直线,而光栅化之后就是 N 个这种颜色的像素点。

游戏

鹿邑:疫情以来游戏股涨势普遍大好,想了解一下我现在学游戏开发来得及吗?

稣:哈呵,游戏其实就是利用游戏引擎,根据玩家们的输入动作渲染声音和画面,很容易理解,不过实际开发起来很多坑,您还是买在港股上市的游戏公司的股票就好。

鹿邑:游戏引擎是什么,渲染又该怎么理解?

稣:游戏引擎是管理图像、声音、人机设备等一套套接口的集合。最重要的是 3D 引擎,简单的说,它是您告诉它“画个圆”,它就帮您“画出一些点,这些点组成一个圆”的接口。单说画面的渲染,可以理解为更多步骤更广义的光栅化,产生显存里的像素点数组,最后扔给显示器。

鹿邑:您又要类比吗?

稣:Look!稣写作的地方都在这些纸上,写得好的,会签名、盖章,拍照拿去发票圈。过程很简单,但包含了几个重要概念。纸张相当于显存;那张拿去拍照发票圈的纸是后台缓冲区(back buffer)的渲染目标(render target);盖章环节相当于是输出合并器阶段(output-merger stage);拍照可以理解为整体光栅化;当然拍照时,摄像头的位置、角度,以及打灯也是有讲究的;最后发票圈就当作输出到显示器吧!

鹿邑:好像不难理解了,您总是把啥都说得很简单,真这么简单吗?

稣:入门难而已!精通,则更难……不过一开始如果把一些专有名词、缩写整明白,可以更快入门。比如 Direct3D 的 API 就存在不少缩写,什么 OM、RS 啊,只看代码不看文档的话很容易懵逼。

鹿邑:数学要很好吧?

稣:数学思维要很好,具体的知识可以现学。几乎每步都有数学的应用,但套路比较有限,可以一通百通。举个最简单的例子:AlphaBlend 算法,常用于把人物、道具合成到背景图里,它其实和盖章是类似的,把章盖在字上,既可以看到字,也可以看到章。其计算公式就是用 alpha 值对背景点、前景点的三原色值做个加减乘运算,理解一下就行,都不用背。由于 YUV 有很多种具体规范,记不住,干脆也甭记,只要明白有些处理 YUV 比 RGB 方便,有些则 RGB 更方便就行。RGB 和 YUV 之间的转化也都有公式,一样只要理解不用记。

鹿邑:您觉得难点在哪里呢?

稣:规范太多!就拿最基础的像素点来说,就存在很多种格式,大的分类就有 RGB 和 YUV 系列,按每个分量(R、G、B、Y、U、V)的排序就可以分出好多种,还有按每个分量几位表示来区分,然后这些位是整数、浮点数或者浮点数规范化的整数,整数是有符号还是无符号都可以造成不同。

鹿邑:停!我已经晕了!

稣:就好像人以群分吧!但可以按照性别、年龄、肤色、籍贯、身高、体重、专业、爱好等各种维度细分,现实世界就有诸多标准,计算机里一样一样的。

鹿邑:明白!人艰不拆,没有容易二字。

稣:该睡了,做梦容易……吓醒。

诗盗·孙子编码规范

《#诗盗#·孙子编码规范》:善编码者,架不重构,洞不三补。码至难者缺人,缺人则贫于招人。财竭则急于裁员。

注解

模仿自春秋时吴国将军孙武的《孙子兵法》:

善用兵者,役不再籍,粮不三载,取用于国,因粮于敌,故军食可足也。
国之贫于师者远输,远输则百姓贫;近师者贵卖,贵卖则百姓财竭,财竭则急于丘役。
力屈、财殚,中原、内虚于家,百姓之费,十去其七;公家之费,破军罢马,甲胄矢弩,戟楯蔽橹,丘牛大车,十去其六。
故智将务食于敌,食敌一钟,当吾二十钟;忌杆一石,当吾二十石。

八哥之神【番外篇9】

听说鲁豫要来采访稣
请戴口罩采访

1. 疫情已经好转很多,终于又可以采访稣!最近在忙什么呢?

忙跳槽。最近两次采访之间,稣一共任职三个公司。

稣现在从事什么行业?

“AIoT 是我的爱,绵绵的金山脚下花正开”难道这歌,稣会唱给你听?

唱的还行,您随意。

“什么样的加班是最呀最摇摆,什么样的养生才是最开怀……”

2. 近期剧情似乎给配角更多笔墨?

是的。这是为了让主角晚点死,是拖剧的惯用套路。

樱国是映射日本吗?

如果您认真阅读,就会发现一个细节:《八哥之神前传【9】》里都是说“樱国”,而《八哥之神前传【10】》中施付说的是“日本”。

是哦,这是不小心写错吗?

不是。这是有区别的。《八哥之神前传【9】》里的故事发生在识界,而《八哥之神前传【10】》的故事则发现在咱们这个世界。

3. 上次您告诉我会出现“李心觎”,但《八哥之神前传【10】》出现的却是“李星觎”,这次是写错了吧?

当然不是。这是剧情需要,后面会解释清楚的,先不剧透。

4. 出现“狐狸精”这种神话色彩的人物,是不是违反您之前说的“无神论”?

这个问题稍微复杂点。这故事发生于 1994 年秋天,圣小开才 12 周岁不到。

他只是个孩子呀!!!

被一系列恐怖的景象吓坏,产生幻觉您可以理解吧!

而且后来开讲给陈因提听时,她并未相信。说明这也可能只是开一脸正经讲的鬼故事而已,作为最了解开的陈因提都如此认为,读者有啥理由拿它当神话看?

原来如此,这可以理解为主角的性格塑造吧!

没错!您想,大部分男人要三四十岁才会遇到狐狸精,而开十岁出头就遇到,而且免疫,这难道不够装逼?

5. 稣,您又赢了!另外,“纯狐连玉”和之前出现的“胡小玉”是同一只吗?

显然不是呀!您看,演员都不是同一个!胡小玉是白色的,长相冷艳,气质是性感魅惑型。纯狐连玉,光看姓,就知道是上古神话风,走的是庄严又有点幽默的大姐路线。

胡小玉只存在于圣小开梦中,是圣小开见过的某类美女的凝神具体。名字很像,主要原因是胡小玉的最初形象是按照纯狐连玉梦见的,后来再也没见过纯狐连玉,形象不断模糊,又不断得到其她美女形象加强就进化出胡小玉。

纯狐连玉是真的狐狸精,而胡小玉更像人。

6. 稣的梦究竟是什么奇妙的世界?能用最贴切的语言描述分享吗?

  • 脑的触感比人体表面最敏感的皮肤还敏感。

  • 爱情里只有鲸神链那部分才是最深刻的。

7. 陈博士的干儿子长生和古思是怎么回事?

孟长生是早期基因编辑婴儿里比较成功的一个,性别染色体来自圣小开,其它基因都来自陈博士,以此为模板优化而成。所以被陈博士收为干儿子,并安排在贾总公司担任一个小领导。

古思是完全基因编辑人造人,只有卵巢是根据陈博士的基因设计,专门用于代孕。这在未来很流行哦!现在大家可能还难以接受。

没关系,等大家有钱再说。

8. 还是有很多读者表示看不懂,您能再提示一下吗?

一部剧能不能看,首先选角很重要,然后靠演员、导演、剧组的努力配合,后期制作、宣传也很重要。

稣只是一个编剧啊!看不懂绝壁不是编剧的问题,您下次采访一下各位演员吧!

八哥之神前传【11】

2042 年

陈博士:长生最近怎么样?

贾力劣:工作尽心尽职无可挑剔,就是好多女员工都打他的主意。

陈博士:难道身份泄露了?

贾力劣:不会!这事严格保密,连他自己都不知道。

陈博士:那就好。

贾力劣:不过您的干儿子年纪也不小,女朋友经常换,坚持不结婚,难免被人说三道四。

陈博士:嗨,这就是基因的力量,思想都差不多。

贾力劣:嗯,他的基因几乎和您一样。

陈博士:他比我还过分,我只是不想生孩子,找个丁克结婚还是可以的。

贾力劣:女朋友换得勤,恐被称渣男呢。

陈博士:他只是个孩子呀!

贾力劣:哦!em……呃!那个,按您基因设计出来的代孕女孩已经送给开哥。

陈博士:很好。这才是正事!管我干儿子的私生活,活腻了?

贾力劣:是是是。她的卵细胞 100% 和您一样。您放一百个心。

陈博士:贾总办事就是靠谱,尤其是保密工作。

贾力劣:懂!相关资料已经销毁。

1994 年

黄金灯已经是著名脑外科专家。他回到虎纠小县城度假的一个夜晚,遇到几个流氓为难大排档卖唱的小妹。

黄金灯:年轻人,文明点!

流氓头:摸个奶,我给钱就是,要你管啊!

黄金灯:小姑娘,你愿意吗?

小姑娘:不!不愿意。

黄金灯:听到没?这位姑娘不卖身。

流氓头:你谁啊?大叔!

黄金灯:我是一名脑外科医生。知道小李飞刀吗?

流氓头:哦?李医生?还飞刀!哈哈哈……

黄金灯亮刀:我这几把叫小灯飞刀,比小李飞刀厉害。

流氓头:大叔,你是不是脑子有问题啊?兄弟们,教训一下他!

黄金灯一出手,飞刀准确插在后面两个流氓鞋头,避开脚趾,将鞋钉在地上,刀拔都拔不动。

流氓发现往前走脚就会被刀切,知道遇到高手,吓得不敢动。

黄金灯:来。我不用飞刀,截拳道对付你。

流氓头跪倒:大叔!小的不识抬举,跟您道歉了。

黄金灯:转过去,跟小姑娘道歉。

小姑娘:大叔怎么称呼?

黄金灯:道释·圣小开。

2003 年

陈因提:死开,你家的牌坊为什么写的是龙田氏?

圣小开:上面写的是:龙田圣心,无尽乾坤。

陈因提:瞎说,上面只有三个字!

圣小开:em?我怎么记得是八个字!

陈因提:又发神经?

圣小开:有可能……晚上安定后告诉你。

1994 年

圣小开出生在道州德国鹰熊岛乾坤村,从小就很好奇村外面是什么。

累积无数次冲动,开终于打算勇敢地往西走,穿过盐田,去看看树木后面是什么!

虽然不远,却感觉走了很久。终于走到树木后面,是一条公路,横穿公路后是一片海沙田,听大人说这田盛产地瓜和花生。

强行穿过海沙田,是小土坡,再翻过去是一些河和另一些盐田,再过去就是海。

开明白,一路向西是无法走到远处的高山。心想:“试试向北,看那片树林后面是不是还是海?”

向右转!走着走着就是一个庙,再过去是灵堂。开想了三秒钟,可怕……还是绕过去吧!

但是开太天真了,虽然绕过灵堂,但它附近有一大堆土坟!于是为了避开它们,居然绕迷路!

平时当开要遇到危险时,都会有路人甲乙冒出来提醒,这次居然没有?

咦?那块石头上面好像有字!走进一看,不禁念出上面的字:“龙田圣心,无尽乾坤”。好酷的感觉!

纯狐连玉:这界碑下面是仙山公。

开转身一看,是个黄色衣装的漂亮的姐姐,便问:仙山公是谁?

纯狐连玉:你干妈的父亲。

圣小开:哦?可是我拜了两位干妈,您说的是哪位?

纯狐连玉:是姑婆祖。

圣小开:哦!原来是族谱第一人。

纯狐连玉:赶紧拜一拜。

圣小开双手合十俯身朝拜:好的。敢问姐姐又是谁?

纯狐连玉:我是仙山公陵墓的守护兽,纯狐连玉。

圣小开:什么?胡连玉?

纯狐连玉:纯狐,连玉。

圣小开:好的,小玉姐姐。

纯狐连玉:死囡仔,按辈分我和你姑婆祖同辈。

圣小开:婆婆好!

纯狐连玉:我是狐狸精。你不害怕?

圣小开:狐狸精?我还是孙悟空转世呢!

纯狐连玉:不怕也好。你跪下,给仙山公磕三个头,我可以实现你一个愿望。

圣小开:这么好康?我要长生不老。

纯狐连玉:死囡仔,活那么久干嘛?三千年就不错啦!

圣小开:成交!我磕。

磕完头,纯狐连玉已经不见。

2003 年

陈因提:狐狸精都出来了!你真能做白日梦!

圣小开:我很严肃好不好!后来我还梦见过它,问它为什么选择我。

陈因提:它怎么说?

圣小开:说我是世间少有的能做很真实的连戏剧梦的人。

陈因提:好像很厉害的样子哦。但是这种能力有什么用?

圣小开:这种能力可以让我从梦中进入全人类,乃至全宇宙所有智慧生命的集体意识中。

陈因提:噗!然后呢?

圣小开:神不会救助任何个体,你懂吧?

陈因提:是啊,要不然神应该来治治你这惊人的幻想能力……

圣小开:不开玩笑,人类社会有很多问题不是在明面可以解决的,或者说这些问题也不需要从明面去解决,而应该通过观测集体意识从而影响它。

陈因提:好吧,你赢了,但是能具体点吗?

圣小开:很多问题的根本都是心灵问题。比如说,残疾人很敏感地发现别人看待他异样的眼光,这可能加强他对自己的嫌弃和遗憾。当你看到别人的问题,一个惊讶、困惑的眼神,对方可能解读成你看到他们差劲的一面,自觉得你会嫌弃他们,于是就烙下一个芥蒂。

陈因提:那你到底能做什么?

圣小开:我可以在睡着的时候进入集体意识去观测大家,让大家的意识明白不管好坏,神都不会遗弃他们。当我醒来世界就会更美好。

陈因提:那你好忙哦,赶紧睡……晚上来观测一下我的意识,看我的意识会不会打你。

圣小开:还好,像我这样的,地球上有 23 人。

CComPtr 和 CComQIPtr

问题

CComPtr 和 CComQIPtr 长得这么像,有啥关系和区别?

分析

  1. 看代码 CComQIPtr 继承自 CComPtr,CComPtr<IUnknown> 没问题,但 CComQIPtr<IUnknown> 报错,应该使用 CComQIPtr<IUnknown, &IID_IUnknown>。

  2. 不同类型 CComPtr<> 不能直接互相构造;CComQIPtr<> 则可以,因为 CComQIPtr 会进行目标类型的 QueryInterface。

1
2
3
4
5
CComPtr<IUnknown> u;
// ...
CComPtr<IDispatch> d(u); // error

CComQIPtr<IDispatch> d(u); // right, will call QueryInterface
  1. 两者构造/赋值时,都会进行 AddRef,如果不想 AddRef,可以使用裸指针(必须十分清楚自己在干嘛!)。
1
2
3
4
5
6
7
CComPtr<IUnknown> u;
// ...
CComPtr<IUnknown> u1(u); // will call AddRef

CComQIPtr<IDispatch> d(u); // will call QueryInterface(call AddRef impliedly)

auto raw = static_cast<IDispatch*>(u.p); // won't call AddRef
  1. 两者赋值时,小部分行为不同。以下模板使得,当等号两边类型不同时,CComPtr 为左值和 CComQIPtr 为左值,表现不同。
1
2
3
4
5
6
7
8
9
10
// CComPtr
template <typename Q>
T* operator=(_Inout_ const CComPtr<Q>& lp) throw()
{
if(!this->IsEqualObject(lp) )
{
AtlComQIPtrAssign2((IUnknown**)&this->p, lp, __uuidof(T));
}
return *this;
}

总结

  • 您可以忘记 CComPtr,只使用 CComQIPtr;

  • 或者,尽量使用 CComPtr,只在必要时使用 CComQIPtr。

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#include <iostream>

#include <atlbase.h>
#include <atlcom.h>
#include <atlstr.h>

MIDL_INTERFACE("00554d55-0000-0000-C000-000000000041")
IA : public IUnknown {
public:
virtual HRESULT STDMETHODCALLTYPE FuncA() = 0;
};

MIDL_INTERFACE("00554d55-0000-0000-C000-000000000042")
IB : public IA {
public:
virtual HRESULT STDMETHODCALLTYPE FuncB() = 0;
};

class A : public IA {
public:
~A() { std::cout << __FUNCTION__ << "\n"; }

HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR* __RPC_FAR* ppvObject) {
if (__uuidof(IA) == riid || __uuidof(IUnknown) == riid) {
*ppvObject = this;
std::cout << __FUNCTION__ << "\n";
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}

ULONG STDMETHODCALLTYPE AddRef(void) {
++ref_;
std::cout << __FUNCTION__ << ": " << ref_ << "\n";
return ref_;
}

ULONG STDMETHODCALLTYPE Release(void) {
--ref_;
std::cout << __FUNCTION__ << ": " << ref_ << "\n";
if (0 == ref_) {
delete this;
}
return ref_;
}

HRESULT STDMETHODCALLTYPE FuncA() {
std::cout << __FUNCTION__ << "\n";
return S_OK;
}

private:
int ref_ = 0;
};

class B : public IB {
public:
~B() { std::cout << __FUNCTION__ << "\n"; }

HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR* __RPC_FAR* ppvObject) {
if (__uuidof(IB) == riid || __uuidof(IUnknown) == riid) {
*ppvObject = this;
std::cout << __FUNCTION__ << "\n";
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}

ULONG STDMETHODCALLTYPE AddRef(void) {
++ref_;
std::cout << __FUNCTION__ << ": " << ref_ << "\n";
return ref_;
}

ULONG STDMETHODCALLTYPE Release(void) {
--ref_;
std::cout << __FUNCTION__ << ": " << ref_ << "\n";
if (0 == ref_) {
delete this;
}
return ref_;
}

HRESULT STDMETHODCALLTYPE FuncA() {
std::cout << __FUNCTION__ << "\n";
return S_OK;
}

HRESULT STDMETHODCALLTYPE FuncB() {
std::cout << __FUNCTION__ << "\n";
return S_OK;
}

private:
int ref_ = 0;
};

HRESULT CreateObject(REFIID riid,
_COM_Outptr_ void __RPC_FAR* __RPC_FAR* ppvObject) {
if (__uuidof(IA) == riid || __uuidof(IUnknown) == riid) {
auto p = new A;
p->QueryInterface(riid, ppvObject);
return S_OK;
} else if (__uuidof(IB) == riid) {
auto p = new B;
p->QueryInterface(riid, ppvObject);
return S_OK;
}
return E_NOINTERFACE;
}

int main() {
std::cout << "---- Raw pointer:\n";
{
IUnknown* u;
HRESULT hr = CreateObject(__uuidof(IA), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
auto a = static_cast<IA*>(u);
a->FuncA();
a->Release();
}
std::cout << "---- ctor\n";
{
CComPtr<IUnknown> u;
HRESULT hr = CreateObject(__uuidof(IUnknown), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
CComPtr<IUnknown> u2(u); // will call AddRef
CComQIPtr<IA> a(u); // will call QueryInterface(call AddRef impliedly)
CComQIPtr<IB> b(a); // will call QueryInterface(call AddRef impliedly)
a->FuncA();
}
std::cout << "---- CComPtr A=B:\n";
{
CComPtr<IUnknown> u;
HRESULT hr = CreateObject(__uuidof(IB), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
CComPtr<IA> a;
a = u; // will call QueryInterface(call AddRef impliedly)
if (a) {
a->FuncA();
}

CComPtr<IB> b;
b = u;
if (b) {
b->FuncA();
b->FuncB();
}

// template<T, Q>
a = b; // failed
if (a) {
std::cout << "failed!\n";
a->FuncA();
}
}
std::cout << "---- CComQIPtr A=CComPtr<B>:\n";
{
CComPtr<IUnknown> u;
HRESULT hr = CreateObject(__uuidof(IB), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
CComQIPtr<IA> a;
a = u; // will call QueryInterface(call AddRef impliedly)
if (a) {
a->FuncA();
}

CComPtr<IB> b;
b = u; // will call QueryInterface(call AddRef impliedly)
if (b) {
b->FuncA();
b->FuncB();
}

a = b; // will call AddRef
if (a) {
std::cout << "OK! B is-a A\n";
a->FuncA();
}
}
std::cout << "---- CComQIPtr A=CComQIPtr<B>:\n";
{
CComPtr<IUnknown> u;
HRESULT hr = CreateObject(__uuidof(IB), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
CComQIPtr<IA> a;
a = u; // will call QueryInterface(call AddRef impliedly)
if (a) {
a->FuncA();
}

CComQIPtr<IB> b;
b = u; // will call QueryInterface(call AddRef impliedly)
if (b) {
b->FuncA();
b->FuncB();
}

a = b; // will call AddRef
if (a) {
std::cout << "OK! B is-a A\n";
a->FuncA();
}
}
std::cout << "---- CComPtr B=A:\n";
{
CComPtr<IUnknown> u;
HRESULT hr = CreateObject(__uuidof(IUnknown), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
CComPtr<IA> a;
a = u; // will call QueryInterface(call AddRef impliedly)
if (a) {
a->FuncA();
}

CComPtr<IB> b;
b = u; // failed
if (b) {
b->FuncA();
b->FuncB();
}
}
std::cout << "---- CComQIPtr B=A:\n";
{
CComPtr<IUnknown> u;
HRESULT hr = CreateObject(__uuidof(IUnknown), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
CComQIPtr<IA> a;
a = u; // will call QueryInterface(call AddRef impliedly)
if (a) {
a->FuncA();
}

CComQIPtr<IB> b;
b = u; // failed
if (b) {
b->FuncA();
b->FuncB();
}
}
std::cout << "---- CComQIPtr<IB>:\n";
{
CComQIPtr<IB> b;
HRESULT hr = CreateObject(__uuidof(IB), reinterpret_cast<void**>(&b));
std::cout << hr << ", " << b << "\n";
b->FuncA();
b->FuncB();
}
std::cout << "---- CComPtr<IA>:\n";
{
CComPtr<IA> a;
HRESULT hr = CreateObject(__uuidof(IB), reinterpret_cast<void**>(&a));
std::cout << hr << ", " << a << "\n";
a->FuncA();
}
std::cout << "---- CComPtr<IUnknown>:\n";
{
CComPtr<IUnknown> u;
HRESULT hr = CreateObject(__uuidof(IA), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
auto a = static_cast<IA*>(u.p);
a->FuncA();
}

return 0;
}