Forwarded from duangsuse
NLP 部分和我们编译原理的解析器理论是相通的,但首先
+ 我只会写比较好看的递归下降法解析器
+ 实际上在 Parser Compiler (Compiler Compiler, 比如 re2c, yacc, bison) 和 Scanner generator 领域(对应 Lex-Yacc Style Parsers)
他们基本都用 NFA, DFA 这种自动机(上面的 NFA, DFA 都只是根据下一个状态属性命名的自动状态机的类型)
实际上,函数式编程向的人们基本都会直接用一段程序的状态替代这种(到状态机匹配指令和状态表的)编译器 + 状态机
而且递归下降法也很快,基于递归下降和 LR 的解析组合子也能解析很多很复杂的文法,基于组合子自动机的也可以自动做诸如左递归消除的解析器优化
+ 不过,这是说编译原理里的解析器理论,和自然语言处理里的还是蛮不一样的
比如说,我们的语法可能是固定的
比如
define num = 1
define hundred = 100
define str = "Hello"
define yes = true define no = false (注意这里不需要换行,看上面的语法规则你就知道为什么)
当然如果 ';' 这个字符被视为空白也可以写成
define yes = true; define no = false
calculate "hello" .. " " .. "world"
calculate 1 + 1
二元表达式链看起来不是多么好办(但是可以左递归什么的,可以直接将优先级大的二元运算符比如 "*" "/" 表达式左右都结合优先级小的),不过自然语言处理要“处理”的问题可比这个简单的递归算法复杂得多
上面括号里的情况:
👆 比如说我们的语法可能是固定的
那 NLP 就不是这种情况喽,看 HanLP 它说它用了 Markov 链,这也是一种机器学习预测算法
看架构 NLP 的分词器显然要和解析器互相协作,依据两方的『猜测』决定最终的算法输出,这是我们编译原理里所没有的,何况它还支持监督学习自动识别单词和单词词性词类呢
不管怎么说建议你先学一下编译原理和简单的机器学习,比如 KNN、决策树、朴素贝叶丝分类器
以及语法,比如中文语法
自然语言处理虽然要做到极致的话,是不得不引入越来越多的分析和学习的,但是做事情也要看程度
LLVM Cookbook 的翻译者也是要学 NLP 的,不过他又半道学了一会编译原理,这是个不错的路径选择
你要学习的话,就赶快先试着自己写一个解释器看看,如果这样的能力都没有的话,自然语言处理是很难入门的,(新手村都出不了,跑)
而且最好先写点 NLP 的应用,比如说,主语推导?(将动词的隐式主语推导出来,显式输出出来)
+ 我只会写比较好看的递归下降法解析器
+ 实际上在 Parser Compiler (Compiler Compiler, 比如 re2c, yacc, bison) 和 Scanner generator 领域(对应 Lex-Yacc Style Parsers)
他们基本都用 NFA, DFA 这种自动机(上面的 NFA, DFA 都只是根据下一个状态属性命名的自动状态机的类型)
实际上,函数式编程向的人们基本都会直接用一段程序的状态替代这种(到状态机匹配指令和状态表的)编译器 + 状态机
而且递归下降法也很快,基于递归下降和 LR 的解析组合子也能解析很多很复杂的文法,基于组合子自动机的也可以自动做诸如左递归消除的解析器优化
+ 不过,这是说编译原理里的解析器理论,和自然语言处理里的还是蛮不一样的
比如说,我们的语法可能是固定的
program = many (_ statement _)猜猜这能匹配什么(当然即使是“辣鸡”的编译原理向解析器也有运算符结合处理的问题,对于不懂递归的人来说这有点烧脑,是真的)
statement
= define | calculate
where
define = string "define" _ identifier _ char '=' _ value
calculate = string "calculate" _ expr
identifier = [a-zA-Z_] [a-zA-Z0-9_]*
value
= litInt | litString | litBoolean
where
litInt = [0-9]+
litString = char '"' char* char '"'
litBoolean = string "true" | string "false"
expr
= intAdd | stringConcat | booleanAnd
where
intAdd = expr '+' expr
stringConcat = expr '..' expr
booleanAnd = expr 'and' expr
_ = spaces
比如
define num = 1
define hundred = 100
define str = "Hello"
define yes = true define no = false (注意这里不需要换行,看上面的语法规则你就知道为什么)
当然如果 ';' 这个字符被视为空白也可以写成
define yes = true; define no = false
calculate "hello" .. " " .. "world"
calculate 1 + 1
二元表达式链看起来不是多么好办(但是可以左递归什么的,可以直接将优先级大的二元运算符比如 "*" "/" 表达式左右都结合优先级小的),不过自然语言处理要“处理”的问题可比这个简单的递归算法复杂得多
上面括号里的情况:
(=)
的前面是规则的名字;后面是规则的模式one = "1"
(|)
组合一组可能的模式,实际上它的结果是这些模式中任何一种 booleanLiteral = "true" | "false"这样实际上在解析规则的模式字面上定义了优先级,结合性问题估计也可以用类似手段去做
Expr = BinAddSub
BinAddSub
= AtomExp ("+" | "-") AtomExp
| BinMulDiv
BinMulDiv
= AtomExp ("*" | "/") AtomExp
| AtomExp
👆 比如说我们的语法可能是固定的
那 NLP 就不是这种情况喽,看 HanLP 它说它用了 Markov 链,这也是一种机器学习预测算法
看架构 NLP 的分词器显然要和解析器互相协作,依据两方的『猜测』决定最终的算法输出,这是我们编译原理里所没有的,何况它还支持监督学习自动识别单词和单词词性词类呢
不管怎么说建议你先学一下编译原理和简单的机器学习,比如 KNN、决策树、朴素贝叶丝分类器
以及语法,比如中文语法
自然语言处理虽然要做到极致的话,是不得不引入越来越多的分析和学习的,但是做事情也要看程度
LLVM Cookbook 的翻译者也是要学 NLP 的,不过他又半道学了一会编译原理,这是个不错的路径选择
你要学习的话,就赶快先试着自己写一个解释器看看,如果这样的能力都没有的话,自然语言处理是很难入门的,(新手村都出不了,跑)
而且最好先写点 NLP 的应用,比如说,主语推导?(将动词的隐式主语推导出来,显式输出出来)
Wikipedia
非确定有限状态自动机
在计算理论中,非确定有限状态自动机或非确定有限自动机(NFA)是对每个状态和输入符号对可以有多个可能的下一个状态的有限状态自动机。这区别于确定有限状态自动机(DFA),它的下一个可能状态是唯一确定的。尽管DFA和NFA有不同的定义,在形式理论中可以证明它们是等价的;就是说,对于任何给定NFA,都可以构造一个等价的DFA,反之亦然:通过使用幂集构造。两种类型的自动机只识别正则语言。非确定有限自动机有时被称为有限类型的子移位(subshift)。非确定有限状态自动机可推广为概率自动机,它为每个状态转移指派概率。
GeekSpec 虽然非常不成熟(而且也没有技术支持...),但是它的确比 Swagger API Tools 的辣鸡 Schema-YAML 语法更优秀,它可以描述的东西是 OpenAPI 3.0 的一个子集
但是!它非常符合直觉,设想一下,如果你要在设计 API 的时候看着一大堆
我都不敢想是什么样子,OpenAPI 由一群连编译原理和 DSL、面向语言编程都没见过的前端『全栈』工程师来设计... 难道就没有人发现写 OpenAPI 的时候很淡疼?即使是 YAML 换了 JSON 也一样?
(不如隔壁 cucumber.io 它还有门 DSL)
对 GeekSpec 来说就不存在这个问题,它的定义式更简洁富于可读性
它是诞生于这条 (https://t.me/dsuse/9017)广播的,我大概花了三四天... 整整三四天啊
对于动辄几十个 HTTP API 接口大一点的应用,直接用 OpenAPI 解决 HTTP 接口定义真的大丈夫?
(GeekSpec 是 Swagger YAML 的子类型、GeekSpec 实现了 Swagger OpenAPI Spec)
而且基于 GeekSpec 的定义上,还有 Spectrum 工具 (https://t.me/dsuse/9120)(和信号处理无关)做辅助:
但是!它非常符合直觉,设想一下,如果你要在设计 API 的时候看着一大堆
非常空阔的缩进行
,你定义一个参数还得按一下缩进,看一个接口有几个参数还得慢慢数,而连自己在设计的接口叫啥名字都不能被很快地看到的话...我都不敢想是什么样子,OpenAPI 由一群连编译原理和 DSL、面向语言编程都没见过的前端『全栈』工程师来设计... 难道就没有人发现写 OpenAPI 的时候很淡疼?即使是 YAML 换了 JSON 也一样?
(不如隔壁 cucumber.io 它还有门 DSL)
对 GeekSpec 来说就不存在这个问题,它的定义式更简洁富于可读性
它是诞生于这条 (https://t.me/dsuse/9017)广播的,我大概花了三四天... 整整三四天啊
searchUser(type:String?{username,nickname,bio}, kw-path:String, sort:String?{created,followers}) -> array:GeekUser想想这类接口,Swagger 又能把它写成几行呢?你写下面这种代码... 又要多花多长时间呢?
= user/search/{kw}
对于动辄几十个 HTTP API 接口大一点的应用,直接用 OpenAPI 解决 HTTP 接口定义真的大丈夫?
getAllUsers() -> array:string = /users(当然这等价于)
GET@getAllUsers() -> array:string = /users如果你会(用 JavaScript 或者 Ruby)写稍微麻烦一点的算法,就能简单地把 GeekSpec 语言的代码转化为 YAML 结构,就可以实现 GeekSpec 语言对 Swagger Tools 的兼容
/users:
get:
summary: Returns a list of users.
description: Optional extended description in CommonMark or HTML.
responses:
"200": # status code
description: A JSON array of user names
content:
application/json:
schema:
type: array
items:
type: string
(GeekSpec 是 Swagger YAML 的子类型、GeekSpec 实现了 Swagger OpenAPI Spec)
而且基于 GeekSpec 的定义上,还有 Spectrum 工具 (https://t.me/dsuse/9120)(和信号处理无关)做辅助:
spectrum(0.6)> status
[+] Total 67 APIs, method count: GET: 37; POST: 8; PUT: 11; DELETE: 11
receiving ["String", "UserId", "Int", "CategoryId", "AppId", "CommentId", "UserSize", "TimelineSize", "NotificationSize", "AppSize", "CommentSize"]
and returning 67 types (["string", "GeekUser", "Category", "App", "AppUpdate", "Timeline", "Notification", "number", "Comment"])
[+] 55 path params, 3 body params
spectrum(0.9)> tree :args
...
POST /admin/makeUser
* username:String
PUT /admin/resetMetaHash/{uid}
* uid-path:UserId
* shash:String?
而且还支持作为测试客户端spectrum(0.1)> listUser
[!] Arity mismatch: expected 3 (not optional 0), got 0
[*] NOTE: fill first optional argument to get started
listUser(sort:String?{created, followers}, sliceFrom:UserSize?, sliceTo:UserSize?) -> array:GeekUser
= GET user/all
spectrum(0.2)> listUser 'created'
=> {"timestamp"=>1549876643013,
"status"=>500,
"error"=>"Internal Server Error",
"message"=>"Optional int parameter 'sliceFrom' is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type.",
"path"=>"/user/all"}
GitHub
duangsuse-valid-projects/GeekApk
GeekApk, the dying SpringBoot(a.k.a. Sping initializr) server for GeekApk(a.k.a 极安) (R - duangsuse-valid-projects/GeekApk
Forwarded from duangsuse
import Control.Applicative
data Move = U | D | L | R
deriving (Eq, Show, Read, Enum)
type Map = [[Bool]]
type Point = (Int, Int)
type Hitsory a = [a]
type TracedRoute = (History Move)
dfs :: Map -> Point -> History Point -> Maybe TracedRoute
dfs map target history step
| step == target = Just []
| step `outbounds` map = Nothing
| blocked or walked = Nothing
| otherwise
= firstJust (fmap try [Up, Down, Left, Right])
where这是个;它递归搜索一个
(x, y) = step
blocked = not (map !! x !! y)
walked = step `elem` history
outbounds p m = let (px, py) = p
in px >= length m or py >= length m !! px
tryDfs m t s = dfs m t [] s
boolean[][]
数组,返回从某一点到指定点的路径否则可以选择向正上/下/左/右方尝试移动
据说某个学校(指 CDLFS,成都某外国语学校)初中的 OI 生都会做
Forwarded from duangsuse
https://github.com/JetBrains/Exposed#sql-dsl-sample
好耶,比某些 Annotation based AOP 框架高到不知哪里去了
然后还可以使用 DataAccessObjects 的风格
好耶,比某些 Annotation based AOP 框架高到不知哪里去了
object Users : Table() {
// Column<out T>
val id = varchar("id", 10).primaryKey()
val name = varchar("name", length = 50)
val cityId = (integer("city_id") references Cities.id).nullable()
}
object Cities : Table() {
val id = integer("id").autoIncrement().primaryKey()
val name = varchar("name", 50)
}
...Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
transaction {
SchemaUtils.create (Cities, Users)
val saintPetersburgId = Cities.insert { it[name] = "St. Petersburg" } get Cities.id
Users.insert { it[id] = "andrey"; it[name] = "Andrey"; it[cityId] = saintPetersburgId }
}
...for (city in Cities.selectAll()) println("${city[Cities.id]}: ${city[Cities.name]}")
本来就应该使用这种风格的然后还可以使用 DataAccessObjects 的风格
object Users : IntIdTable() {
val name = varchar("name", 50).index()
val city = reference("city", Cities)
val age = integer("age")
}
class User(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<User>(Users)
var name by Users.name
var city by (City referencedOn Users.city).nullable()
var age by Users.age
}
...User.new {
name = "duangsuse"
age = 17
}
GitHub
GitHub - JetBrains/Exposed: Kotlin SQL Framework
Kotlin SQL Framework. Contribute to JetBrains/Exposed development by creating an account on GitHub.
Forwarded from duangsuse
关键是这个判断
The strcpy() function copies the string pointed to by src, including the terminating null byte ('\0')
If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated.
但是 strlen 不包含 NUL byte '\0' 长度, 所以作者 strncpy 时加上是正确的
但这样(调用
这是不安全的操作,所以 CC 阻止了编译,但它是没有问题的
找不到 inlen 的话就定义栈帧
if (retv && inlen >= value_len + 1)
// strncpy (retv, value, strlen(value) +1);原来可能就没有,所以有时候 strncpy (它的意思是从 src 复制 n 个 char 到 dst,这里 C 的『字符串长度』因为字符串是 NUL 结尾的,得加上 NUL 字符的长度 1,可惜作者弄错了或者说故意弄的,原因在后面)
The strcpy() function copies the string pointed to by src, including the terminating null byte ('\0')
strncpy:
If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated.
但是 strlen 不包含 NUL byte '\0' 长度, 所以作者 strncpy 时加上是正确的
但这样(调用
strncpy
, 写到指针, 但是指针是不定长无限制的)实际上就可能 overflow 了 retv: 本来它可能(是在其他函数里)分配了未知长度的内存,可是我们居然可以随便按照 value 字符串的长度复制这是不安全的操作,所以 CC 阻止了编译,但它是没有问题的
找不到 inlen 的话就定义栈帧
auto
变量size_t inlen = strlen(retv);它可以 suppress
size_t value_len = strlen(value);
看起来就算是没有刻意用 GoF 的 OOP 设计模式,你也是会正常的 OO 吗,难道 FlarumSDK 的
public class FlarumException extends Exception
int status, code; List<Error> allErrors;
public class Result<T>
int id;
okhttp3.Response rawResponse;
JSONAPIObject object;
T mainAttr;
public class RequestBuilder<T>
final Flarum flarum;
final ObjectParser.JsonObjectConverter<TYPE> converter;
final String method, urlEndpoint;
final RequestBody body;
public static class RequestBuilder$BaseRequest<TYPE>
private okhttp3.Request.Builder baseBuilder ()
这一类虽然算法比较简单(几个顺序分支;和 Kotlin 里
public interface Callback<T> 这个看起来像是复制的(
可是怎么看都用了正常的面向对象编程啊;media type & converter、token getter、tasks & executor
还有 JSON response parser, Json Object converter...
这类代码都不能算是了解面向对象编程了吗,看起来菜鸡是不可能哪怕是不包括型变的泛型和 converter 这种东西...(虽然 converter 好像懂泛型能抽提就可以了)
要是真正的菜鸡大概是这些都不可能会的
public class FlarumException extends Exception
int status, code; List<Error> allErrors;
public class Result<T>
int id;
okhttp3.Response rawResponse;
JSONAPIObject object;
T mainAttr;
public class RequestBuilder<T>
final Flarum flarum;
final ObjectParser.JsonObjectConverter<TYPE> converter;
final String method, urlEndpoint;
final RequestBody body;
public static class RequestBuilder$BaseRequest<TYPE>
private okhttp3.Request.Builder baseBuilder ()
这一类虽然算法比较简单(几个顺序分支;和 Kotlin 里
thread
辅助函数的差不多,按需设置对象)但是没有一点意识也是写不出来的啦public interface Callback<T> 这个看起来像是复制的(
可是怎么看都用了正常的面向对象编程啊;media type & converter、token getter、tasks & executor
还有 JSON response parser, Json Object converter...
这类代码都不能算是了解面向对象编程了吗,看起来菜鸡是不可能哪怕是不包括型变的泛型和 converter 这种东西...(虽然 converter 好像懂泛型能抽提就可以了)
要是真正的菜鸡大概是这些都不可能会的
GitHub
Trumeet/FlarumSDK
Flarum SDK for Java and Android. Contribute to Trumeet/FlarumSDK development by creating an account on GitHub.
Forwarded from duangsuse
文档在这里... 刚刚看了一下 Kotlin 语义上 execute 后两个参数是 producer 和 job 都在 worker 里执行,执行完可以给 futuer 的 consumer 处理
然后
看起来像是没有参数的,不过我知道其实第二个块有一个默认参数
但是我觉得它是在说第一个没有参数的块...
上面的 message 是告诉你:这个(编译后)的函数指针不能有参数,比如 self、比如 captures
所以... 我们看看 Worker#execute 的实现算了
首先 Kotlin 的 Instrinsics 函数 external internal fun executeInternal
execute 它是编译器的 Instrinsics 方法...
然后
kotlin.native.concurrent.Worker.execute must take an unbound, non-capturing function or lambda
emmm...val descriptor = expression.descriptor.original
if (irCallableReference == null || irCallableReference.getArguments().isNotEmpty())
??? 🌚看起来像是没有参数的,不过我知道其实第二个块有一个默认参数
it
(T2
)但是我觉得它是在说第一个没有参数的块...
上面的 message 是告诉你:这个(编译后)的函数指针不能有参数,比如 self、比如 captures
所以... 我们看看 Worker#execute 的实现算了
首先 Kotlin 的 Instrinsics 函数 external internal fun executeInternal
execute 它是编译器的 Instrinsics 方法...
@TypedIntrinsic(IntrinsicType.WORKER_EXECUTE)
就是我刚才给你看的具体实现Kotlin
Worker - Kotlin Programming Language
Forwarded from duangsuse
比如这个 onclick 其实也就是缩短了一点标识符而已
只是说对编译原理的学习者来说,IDE 的重构功能肯定是不会缺少的,何况工具也会多一点,这样专门的反混淆器也不难开发的
(j :: DOMElement).onclick = () => { it.forEach((x) => {实际上就是让你把
if (x === h.value) return f(m.err_uin)
if (x === i.value) return f(m.err_password)
});
}
f(_)
的逻辑内联到这里面来,程序变换的方法很多的,比如 f
是这样的const f = (res) => switch (res) { case m.err_uin: console.log('error') }最后实际上
onclick
逻辑就等价j.onclick = () => { it.forEach(
(x) => {
if (x === i.value) console.log('error')
//if (x === h.value) {}
});
}
!1
是对二进制数值 0b1 取反,但是没有多大用处,可以去掉就好了,不过也要相应的去除一些数值只是说对编译原理的学习者来说,IDE 的重构功能肯定是不会缺少的,何况工具也会多一点,这样专门的反混淆器也不难开发的
Forwarded from duangsuse
指针一直以来不是最高大上的概念啊,稍微对现代电子计算机存储器结构、冯诺伊曼计算机结构有点感觉的人都会觉得指针实际上非常符合直觉
因为内存就是一大打能够以某种最小粒度(很多时候是
然后 C 只要不弄出悬垂指针、空指针也是最吼滴,对新手最大的问题可能是动态内存分配和“为什么我不能返回一个本地变量
然后 C 的
在编程语言理论里,
当
(当然这个函数也可以用 C11 的新特性
然后那个
要是刻意指定可以写
因为内存就是一大打能够以某种最小粒度(很多时候是
char
,一个字节)访问(读写)存储器的集合而已,只要你知道可随机访问的存储器一般以某种(整型数值上)连续的方式编址可访问就好了然后 C 只要不弄出悬垂指针、空指针也是最吼滴,对新手最大的问题可能是动态内存分配和“为什么我不能返回一个本地变量
char *a
”?(他们实际上看不到运行时有个叫做栈(stack) 的东西.... 也不知道有 GC, garbage collector 编程语言里所谓的对象在 C 里都是必须弄明白他们到底应该被『存储』在哪里的,要不然下面的机器没办法执行)然后 C 的
struct
, union
, array 什么的内存布局也没有那么奇怪,也提供了 &*
直接对其他对象取地址的方式,不会指针运算双重指针(int **
)也总是会的吧。void swap(int *a, int *b) {注意这里
auto int saved_a = *a;
*a = *b;
*b = saved_a;
}
*
有两种含义:在编程语言理论里,
int*
类型的东西是可以同时作为左值 (lhs, {get;set;}
) 和右值 (rhs {get;}
) 使用的(而 const int*
就不可以,只能当左值取值不能赋值)当
*a
出现在需要左值的位置时,*
操作符的语义实际上是『“ptr write”』给指针赋值,比如 *a=-1;
反之,它的含义是解指针(ptr read)比如上面的 *a = *b;等号右边
*b
就是 deref 操作。(当然这个函数也可以用 C11 的新特性
_Generic
写,不过这里是作为示范就先不写)然后那个
auto
是存储位置的修饰符,这个是默认的,表示不刻意指定要是刻意指定可以写
register
机器寄存器、static
常量(一般存在 ELF 的 .bss
, block started by symbol 端)、extern
外部编译单元可见等等Forwarded from duangsuse
KN 不是基于 JVM 的,对于 executable 它翻译到 LLVM 表示后就直接发射机器码了,但是你可以注意到 KN 也提供 GC
KN 远比 JVM 上的 Kotlin 提供的底层控制多,比如它好像还可以给本地栈指定什么参数?
runtime/src/main/cpp/Memory.h
111: 00000000002288c0 5 FUNC GLOBAL DEFAULT 15 Kotlin_createRuntime
(注意这不是 java.lang.Runtime)
我上次写的没有用到动态内存管理,但是 KN 是有 runtime 的,它不像 Rust 的 runtime 很小,而且甚至可以去掉,毕竟是要和 JVM/JS 版兼容的
KN 远比 JVM 上的 Kotlin 提供的底层控制多,比如它好像还可以给本地栈指定什么参数?
runtime/src/main/cpp/Memory.h
readelf -s ./build/bin/linuxX64/releaseExecutable/secure-desktop.kexe|grep Kotlin102: 00000000002289b0 146 FUNC GLOBAL DEFAULT 15 Kotlin_destroyRuntime
111: 00000000002288c0 5 FUNC GLOBAL DEFAULT 15 Kotlin_createRuntime
(注意这不是 java.lang.Runtime)
我上次写的没有用到动态内存管理,但是 KN 是有 runtime 的,它不像 Rust 的 runtime 很小,而且甚至可以去掉,毕竟是要和 JVM/JS 版兼容的
GitHub
JetBrains/kotlin-native
Kotlin/Native infrastructure. Contribute to JetBrains/kotlin-native development by creating an account on GitHub.
Forwarded from Richard Yu
因为不是C/C++,谁知道有没有指针?没的话那就传个0吧。C++我会写nullptr。
Forwarded from duangsuse
如果没有指针类型的话也是可能的,但是,如果是我也会搞个全局的常量
如果类型系统菜,程序员负责给它洗地
nullptr
, 尽可能避免混淆如果类型系统菜,程序员负责给它洗地
Forwarded from duangsuse
是啊,所以我说是一个常量
const char *EMPTY_CHARP = "";
这里我没有指定具体分配位置,但它是常量,换句话说编译器喜欢内联也可以直接翻译成GetModuleHandle("");或者
const static char EMPTY_CHARP[] = {'\0'};
然后 GetModuleHandle(EMPTY_CHARP);