mirror of
https://onedev.site.tesses.net/crosslang
synced 2026-04-18 14:37:02 +00:00
Make crosslang more type safe
This commit is contained in:
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -8,8 +8,8 @@
|
|||||||
"name": "(gdb) Launch",
|
"name": "(gdb) Launch",
|
||||||
"type": "cppdbg",
|
"type": "cppdbg",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "/usr/local/bin/crossint",
|
"program": "${workspaceFolder}/builds/linux/crosslang",
|
||||||
"args": ["/home/mike/"],
|
"args": ["token"],
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"environment": [],
|
"environment": [],
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ src/vm/filereader.cpp
|
|||||||
src/vm/gc.cpp
|
src/vm/gc.cpp
|
||||||
src/vm/gclist.cpp
|
src/vm/gclist.cpp
|
||||||
src/vm/vm.cpp
|
src/vm/vm.cpp
|
||||||
src/bitconverter.cpp
|
|
||||||
src/archive.cpp
|
src/archive.cpp
|
||||||
src/markedtobject.cpp
|
src/markedtobject.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
namespace Tesses::CrossLang {
|
namespace Tesses::CrossLang {
|
||||||
|
using BitConverter = Tesses::Framework::Serialization::BitConverter;
|
||||||
constexpr std::string_view VMName = "CrossLangVM";
|
constexpr std::string_view VMName = "CrossLangVM";
|
||||||
constexpr std::string_view VMHowToGet = "https://crosslang.tesseslanguage.com/";
|
constexpr std::string_view VMHowToGet = "https://crosslang.tesseslanguage.com/";
|
||||||
/**
|
/**
|
||||||
@@ -719,48 +720,6 @@ class SimpleInstruction : public ByteCodeInstruction {
|
|||||||
void Write(std::vector<uint8_t>& data);
|
void Write(std::vector<uint8_t>& data);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief A bit converter
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class BitConverter {
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* @brief Get the bits of a double from a int64_t
|
|
||||||
*
|
|
||||||
* @param v a int64_t with double's bits
|
|
||||||
* @return double the double
|
|
||||||
*/
|
|
||||||
static double ToDoubleBits(uint64_t v);
|
|
||||||
/**
|
|
||||||
* @brief Get the bits of a int64_t from a double
|
|
||||||
*
|
|
||||||
* @param v a double with int64_t's bits
|
|
||||||
* @return uint64_t the int64_t
|
|
||||||
*/
|
|
||||||
static uint64_t ToUintBits(double v);
|
|
||||||
/**
|
|
||||||
* @brief Get big endian double from uint8_t reference of first element of 8 byte array
|
|
||||||
*
|
|
||||||
* @param b a reference to the first byte of an array
|
|
||||||
* @return double the double
|
|
||||||
*/
|
|
||||||
static double ToDoubleBE(uint8_t& b);
|
|
||||||
/**
|
|
||||||
* @brief Get big endian uint64_t from uint8_t reference of first element of 8 byte array
|
|
||||||
*
|
|
||||||
* @param b a reference to the first byte of an array
|
|
||||||
* @return uint64_t the uint64_t
|
|
||||||
*/
|
|
||||||
static uint64_t ToUint64BE(uint8_t& b);
|
|
||||||
static uint32_t ToUint32BE(uint8_t& b);
|
|
||||||
static uint16_t ToUint16BE(uint8_t& b);
|
|
||||||
static void FromDoubleBE(uint8_t& b, double v);
|
|
||||||
static void FromUint64BE(uint8_t& b, uint64_t v);
|
|
||||||
static void FromUint32BE(uint8_t& b, uint32_t v);
|
|
||||||
static void FromUint16BE(uint8_t& b, uint16_t v);
|
|
||||||
};
|
|
||||||
|
|
||||||
class StringInstruction : public ByteCodeInstruction {
|
class StringInstruction : public ByteCodeInstruction {
|
||||||
public:
|
public:
|
||||||
uint32_t n;
|
uint32_t n;
|
||||||
@@ -1763,6 +1722,7 @@ class GC {
|
|||||||
bool HasValue(std::string className,std::string name);
|
bool HasValue(std::string className,std::string name);
|
||||||
bool HasField(std::string className,std::string name);
|
bool HasField(std::string className,std::string name);
|
||||||
bool HasMethod(std::string className,std::string name);
|
bool HasMethod(std::string className,std::string name);
|
||||||
|
TObject CallMethod(GCList& ls, std::string className, std::string name,std::vector<TObject> args);
|
||||||
std::string TypeName();
|
std::string TypeName();
|
||||||
void Mark();
|
void Mark();
|
||||||
};
|
};
|
||||||
@@ -2034,32 +1994,57 @@ class GC {
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Tesses::Framework::Streams::Stream* stream;
|
Tesses::Framework::Streams::Stream* stream;
|
||||||
|
std::vector<TObject> watch;
|
||||||
|
|
||||||
static TStreamHeapObject* Create(GCList& ls, Tesses::Framework::Streams::Stream* strm);
|
static TStreamHeapObject* Create(GCList& ls, Tesses::Framework::Streams::Stream* strm);
|
||||||
static TStreamHeapObject* Create(GCList* ls, Tesses::Framework::Streams::Stream* strm);
|
static TStreamHeapObject* Create(GCList* ls, Tesses::Framework::Streams::Stream* strm);
|
||||||
~TStreamHeapObject();
|
~TStreamHeapObject();
|
||||||
void Close();
|
void Close();
|
||||||
|
void Mark()
|
||||||
|
{
|
||||||
|
if(this->marked) return;
|
||||||
|
this->marked=true;
|
||||||
|
for(auto item : watch)
|
||||||
|
GC::Mark(item);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class TVFSHeapObject : public THeapObject
|
class TVFSHeapObject : public THeapObject
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Tesses::Framework::Filesystem::VFS* vfs;
|
Tesses::Framework::Filesystem::VFS* vfs;
|
||||||
|
std::vector<TObject> watch;
|
||||||
static TVFSHeapObject* Create(GCList& ls, Tesses::Framework::Filesystem::VFS* vfs);
|
static TVFSHeapObject* Create(GCList& ls, Tesses::Framework::Filesystem::VFS* vfs);
|
||||||
static TVFSHeapObject* Create(GCList* ls, Tesses::Framework::Filesystem::VFS* vfs);
|
static TVFSHeapObject* Create(GCList* ls, Tesses::Framework::Filesystem::VFS* vfs);
|
||||||
~TVFSHeapObject();
|
~TVFSHeapObject();
|
||||||
void Close();
|
void Close();
|
||||||
|
|
||||||
|
void Mark()
|
||||||
|
{
|
||||||
|
if(this->marked) return;
|
||||||
|
this->marked=true;
|
||||||
|
for(auto item : watch)
|
||||||
|
GC::Mark(item);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class TServerHeapObject : public THeapObject
|
class TServerHeapObject : public THeapObject
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Tesses::Framework::Http::IHttpServer* server;
|
Tesses::Framework::Http::IHttpServer* server;
|
||||||
|
std::vector<TObject> watch;
|
||||||
static TServerHeapObject* Create(GCList& ls, Tesses::Framework::Http::IHttpServer* vfs);
|
static TServerHeapObject* Create(GCList& ls, Tesses::Framework::Http::IHttpServer* vfs);
|
||||||
static TServerHeapObject* Create(GCList* ls, Tesses::Framework::Http::IHttpServer* vfs);
|
static TServerHeapObject* Create(GCList* ls, Tesses::Framework::Http::IHttpServer* vfs);
|
||||||
~TServerHeapObject();
|
~TServerHeapObject();
|
||||||
void Close();
|
void Close();
|
||||||
|
bool Handle(std::vector<TObject> args);
|
||||||
|
void Mark()
|
||||||
|
{
|
||||||
|
if(this->marked) return;
|
||||||
|
this->marked=true;
|
||||||
|
for(auto item : watch)
|
||||||
|
GC::Mark(item);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
class TObjectVFS : public Tesses::Framework::Filesystem::VFS
|
class TObjectVFS : public Tesses::Framework::Filesystem::VFS
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
#include "CrossLang.hpp"
|
|
||||||
|
|
||||||
namespace Tesses::CrossLang
|
|
||||||
{
|
|
||||||
double BitConverter::ToDoubleBits(uint64_t v)
|
|
||||||
{
|
|
||||||
return *(double*)&v;
|
|
||||||
}
|
|
||||||
uint64_t BitConverter::ToUintBits(double v)
|
|
||||||
{
|
|
||||||
return *(uint64_t*)&v;
|
|
||||||
}
|
|
||||||
double BitConverter::ToDoubleBE(uint8_t& b)
|
|
||||||
{
|
|
||||||
return ToDoubleBits(ToUint64BE(b));
|
|
||||||
}
|
|
||||||
uint64_t BitConverter::ToUint64BE(uint8_t& b)
|
|
||||||
{
|
|
||||||
uint8_t* b2 = &b;
|
|
||||||
uint64_t v = 0;
|
|
||||||
v |= ((uint64_t)b2[0] << 56);
|
|
||||||
v |= ((uint64_t)b2[1] << 48);
|
|
||||||
v |= ((uint64_t)b2[2] << 40);
|
|
||||||
v |= ((uint64_t)b2[3] << 32);
|
|
||||||
v |= ((uint64_t)b2[4] << 24);
|
|
||||||
v |= ((uint64_t)b2[5] << 16);
|
|
||||||
v |= ((uint64_t)b2[6] << 8);
|
|
||||||
v |= (uint64_t)b2[7];
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
uint32_t BitConverter::ToUint32BE(uint8_t& b)
|
|
||||||
{
|
|
||||||
uint8_t* b2 = &b;
|
|
||||||
uint32_t v = 0;
|
|
||||||
|
|
||||||
v |= ((uint32_t)b2[0] << 24);
|
|
||||||
v |= ((uint32_t)b2[1] << 16);
|
|
||||||
v |= ((uint32_t)b2[2] << 8);
|
|
||||||
v |= (uint32_t)b2[3];
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
uint16_t BitConverter::ToUint16BE(uint8_t& b)
|
|
||||||
{
|
|
||||||
uint8_t* b2 = &b;
|
|
||||||
uint16_t v = 0;
|
|
||||||
|
|
||||||
|
|
||||||
v |= ((uint16_t)b2[0] << 8);
|
|
||||||
v |= (uint16_t)b2[1];
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
void BitConverter::FromDoubleBE(uint8_t& b, double v)
|
|
||||||
{
|
|
||||||
FromUint64BE(b,ToUintBits(v));
|
|
||||||
}
|
|
||||||
void BitConverter::FromUint64BE(uint8_t& b, uint64_t v)
|
|
||||||
{
|
|
||||||
uint8_t* b2 = &b;
|
|
||||||
b2[0] = (uint8_t)(v >> 56);
|
|
||||||
b2[1] = (uint8_t)(v >> 48);
|
|
||||||
b2[2] = (uint8_t)(v >> 40);
|
|
||||||
b2[3] = (uint8_t)(v >> 32);
|
|
||||||
b2[4] = (uint8_t)(v >> 24);
|
|
||||||
b2[5] = (uint8_t)(v >> 16);
|
|
||||||
b2[6] = (uint8_t)(v >> 8);
|
|
||||||
b2[7] = (uint8_t)v;
|
|
||||||
}
|
|
||||||
void BitConverter::FromUint32BE(uint8_t& b, uint32_t v)
|
|
||||||
{
|
|
||||||
uint8_t* b2 = &b;
|
|
||||||
|
|
||||||
b2[0] = (uint8_t)(v >> 24);
|
|
||||||
b2[1] = (uint8_t)(v >> 16);
|
|
||||||
b2[2] = (uint8_t)(v >> 8);
|
|
||||||
b2[3] = (uint8_t)v;
|
|
||||||
}
|
|
||||||
void BitConverter::FromUint16BE(uint8_t& b, uint16_t v)
|
|
||||||
{
|
|
||||||
uint8_t* b2 = &b;
|
|
||||||
|
|
||||||
b2[0] = (uint8_t)(v >> 8);
|
|
||||||
b2[1] = (uint8_t)v;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
|
||||||
namespace Tesses::CrossLang
|
namespace Tesses::CrossLang
|
||||||
{
|
{
|
||||||
void Write(Tesses::Framework::Streams::Stream* strm, uint8_t* buffer, size_t len)
|
void Write(Tesses::Framework::Streams::Stream* strm, uint8_t* buffer, size_t len)
|
||||||
|
|||||||
@@ -220,9 +220,10 @@ namespace Tesses::CrossLang {
|
|||||||
dict->DeclareFunction(gc,"Read", "Reads a byte from stdin",{},Console_Read);
|
dict->DeclareFunction(gc,"Read", "Reads a byte from stdin",{},Console_Read);
|
||||||
dict->DeclareFunction(gc,"ReadLine","Reads line from stdin",{},Console_ReadLine);
|
dict->DeclareFunction(gc,"ReadLine","Reads line from stdin",{},Console_ReadLine);
|
||||||
dict->DeclareFunction(gc,"Write","Write text \"text\" to stdout",{"text"},Console_Write);
|
dict->DeclareFunction(gc,"Write","Write text \"text\" to stdout",{"text"},Console_Write);
|
||||||
dict->DeclareFunction(gc,"WriteLine","Write text \"text\" to stdout with new line",{"text"},Console_WriteLine);
|
dict->DeclareFunction(gc,"WriteLine","Write text \"text\" to stdout with new line",{"$text"},Console_WriteLine);
|
||||||
dict->DeclareFunction(gc,"Error", "Write text \"error\" to stderr",{"error"},Console_Error);
|
dict->DeclareFunction(gc,"Error", "Write text \"error\" to stderr",{"error"},Console_Error);
|
||||||
dict->DeclareFunction(gc,"ErrorLine","Write text \"error\" to stderr",{"error"},Console_ErrorLine);
|
dict->DeclareFunction(gc,"ErrorLine","Write text \"error\" to stderr",{"$error"},Console_ErrorLine);
|
||||||
|
if(env->permissions.canRegisterEverything)
|
||||||
dict->DeclareFunction(gc,"Fatal","Stop the program with an optional error message",{"$text"},Console_Fatal);
|
dict->DeclareFunction(gc,"Fatal","Stop the program with an optional error message",{"$text"},Console_Fatal);
|
||||||
dict->DeclareFunction(gc,"getIn","Get stdin Stream",{},Console_getIn);
|
dict->DeclareFunction(gc,"getIn","Get stdin Stream",{},Console_getIn);
|
||||||
dict->DeclareFunction(gc,"getOut","Get stdout Stream",{},Console_getOut);
|
dict->DeclareFunction(gc,"getOut","Get stdout Stream",{},Console_getOut);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
#include "CrossLang.hpp"
|
#include "CrossLang.hpp"
|
||||||
#if defined(CROSSLANG_ENABLE_SHARED)
|
#if defined(CROSSLANG_ENABLE_SHARED)
|
||||||
#if defined(CROSSLANG_ENABLE_FFI)
|
#if defined(CROSSLANG_ENABLE_FFI)
|
||||||
@@ -30,6 +31,35 @@
|
|||||||
|
|
||||||
namespace Tesses::CrossLang
|
namespace Tesses::CrossLang
|
||||||
{
|
{
|
||||||
|
class TMuxex : public TNativeObject
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Tesses::Framework::Threading::Mutex mtx;
|
||||||
|
|
||||||
|
std::string TypeName()
|
||||||
|
{
|
||||||
|
return "Mutex";
|
||||||
|
}
|
||||||
|
|
||||||
|
TObject CallMethod(GCList& ls, std::string key, std::vector<TObject> args)
|
||||||
|
{
|
||||||
|
if(key == "Lock")
|
||||||
|
{
|
||||||
|
mtx.Lock();
|
||||||
|
}
|
||||||
|
else if(key == "Unlock")
|
||||||
|
{
|
||||||
|
mtx.Unlock();
|
||||||
|
}
|
||||||
|
else if(key == "TryLock")
|
||||||
|
{
|
||||||
|
return mtx.TryLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Undefined();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class DocumentationParser : public TNativeObject
|
class DocumentationParser : public TNativeObject
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -1284,48 +1314,8 @@ namespace Tesses::CrossLang
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
});
|
});
|
||||||
newTypes->DeclareFunction(gc, "Mutex", "Create mutex",{}, [](GCList& ls,std::vector<TObject> args)->TObject {
|
newTypes->DeclareFunction(gc, "Mutex", "Create mutex",{}, [](GCList& ls,std::vector<TObject> args)->TObject {
|
||||||
ls.GetGC()->BarrierBegin();
|
|
||||||
auto mtx = TDictionary::Create(ls);
|
|
||||||
auto native = TNative::Create(ls, new Tesses::Framework::Threading::Mutex(),[](void* ptr)->void{
|
|
||||||
delete static_cast<Tesses::Framework::Threading::Mutex*>(ptr);
|
|
||||||
});
|
|
||||||
auto lock = TExternalMethod::Create(ls,"Lock the mutex",{},[native](GCList& ls, std::vector<TObject> args)->TObject {
|
|
||||||
if(native->GetDestroyed()) return nullptr;
|
|
||||||
auto r = static_cast<Tesses::Framework::Threading::Mutex*>(native->GetPointer());
|
|
||||||
r->Lock();
|
|
||||||
return nullptr;
|
|
||||||
});
|
|
||||||
lock->watch.push_back(native);
|
|
||||||
mtx->SetValue("Lock",lock);
|
|
||||||
|
|
||||||
auto unlock = TExternalMethod::Create(ls,"Unlock the mutex",{},[native](GCList& ls, std::vector<TObject> args)->TObject {
|
return TNativeObject::Create<TMuxex>(ls);
|
||||||
if(native->GetDestroyed()) return nullptr;
|
|
||||||
auto r = static_cast<Tesses::Framework::Threading::Mutex*>(native->GetPointer());
|
|
||||||
r->Unlock();
|
|
||||||
return nullptr;
|
|
||||||
});
|
|
||||||
unlock->watch.push_back(native);
|
|
||||||
mtx->SetValue("Unlock",unlock);
|
|
||||||
|
|
||||||
|
|
||||||
auto trylock = TExternalMethod::Create(ls,"Try to lock the mutex, returns true if we aquire the lock, false if we can't due to another thread owning it",{},[native](GCList& ls, std::vector<TObject> args)->TObject {
|
|
||||||
if(native->GetDestroyed()) return true;
|
|
||||||
auto r = static_cast<Tesses::Framework::Threading::Mutex*>(native->GetPointer());
|
|
||||||
return r->TryLock();
|
|
||||||
});
|
|
||||||
trylock->watch.push_back(native);
|
|
||||||
mtx->SetValue("TryLock",trylock);
|
|
||||||
ls.GetGC()->BarrierEnd();
|
|
||||||
|
|
||||||
|
|
||||||
auto close = TExternalMethod::Create(ls,"Try to lock the mutex, returns true if we aquire the lock, false if we can't due to another thread owning it",{},[native](GCList& ls, std::vector<TObject> args)->TObject {
|
|
||||||
native->Destroy();
|
|
||||||
return nullptr;
|
|
||||||
});
|
|
||||||
close->watch.push_back(native);
|
|
||||||
mtx->SetValue("Close",close);
|
|
||||||
ls.GetGC()->BarrierEnd();
|
|
||||||
return mtx;
|
|
||||||
});
|
});
|
||||||
newTypes->DeclareFunction(gc, "Thread","Create thread",{"callback"},[](GCList& ls, std::vector<TObject> args)-> TObject
|
newTypes->DeclareFunction(gc, "Thread","Create thread",{"callback"},[](GCList& ls, std::vector<TObject> args)-> TObject
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ namespace Tesses::CrossLang
|
|||||||
for(auto& item : this->inherit_tree) type += " : " + item;
|
for(auto& item : this->inherit_tree) type += " : " + item;
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
TObject TClassObject::CallMethod(GCList& ls, std::string className, std::string name,std::vector<TObject> args)
|
||||||
|
{
|
||||||
|
auto value = this->GetValue(className,name);
|
||||||
|
TCallable* callable;
|
||||||
|
if(GetObjectHeap(value, callable))
|
||||||
|
{
|
||||||
|
return callable->Call(ls,args);
|
||||||
|
}
|
||||||
|
return Undefined();
|
||||||
|
}
|
||||||
TClassObjectEntry* TClassObject::GetEntry(std::string classN, std::string key)
|
TClassObjectEntry* TClassObject::GetEntry(std::string classN, std::string key)
|
||||||
{
|
{
|
||||||
for(auto& item : this->entries)
|
for(auto& item : this->entries)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
#include "CrossLang.hpp"
|
#include "CrossLang.hpp"
|
||||||
#include <TessesFramework/Filesystem/VFS.hpp>
|
#include <TessesFramework/Filesystem/VFS.hpp>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
@@ -3253,28 +3254,7 @@ namespace Tesses::CrossLang {
|
|||||||
}
|
}
|
||||||
if(key == "Handle")
|
if(key == "Handle")
|
||||||
{
|
{
|
||||||
|
cse.back()->Push(gc,svr->Handle(args));
|
||||||
if(GetArgumentHeap(args,0,dict))
|
|
||||||
{
|
|
||||||
gc->BarrierBegin();
|
|
||||||
auto nat = dict->GetValue("native");
|
|
||||||
gc->BarrierEnd();
|
|
||||||
TNative* nat2;
|
|
||||||
if(GetObjectHeap(nat,nat2))
|
|
||||||
{
|
|
||||||
if(!nat2->GetDestroyed())
|
|
||||||
{
|
|
||||||
auto ctx = static_cast<Tesses::Framework::Http::ServerContext*>(nat2->GetPointer());
|
|
||||||
if(ctx != nullptr)
|
|
||||||
{
|
|
||||||
cse.back()->Push(gc,svr->server->Handle(*ctx));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cse.back()->Push(gc,false);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(key == "Close")
|
if(key == "Close")
|
||||||
|
|||||||
Reference in New Issue
Block a user